This notebook illustrates how one can learn parameters using stochastic gradient descent. This will demonstrate the basic idea of how iterating over forward and backward passes improves our loss function.

A couple things to note. First, this is note building a neural net from scratch; rather, it is using stochasti gradient descent to build a simple linear regression model which would be analogous to building a single perceptron. Second, this notebook does not focus on efficient implementation of this algorithm; rather, the code is designed to make the concept intuitive to the reader.

Requirements

library(tidyverse)
library(glue)

Creating our data

For simplicity we are going to use a very simple data set that is generated based on a simple linear model without any error. Our data includes:

n <- 30
b <- 30
a <- 2

set.seed(123)
(df <- tibble(
  x = runif(30, 0, 100), 
  y = b + a*x
  ))

The following plot shows how an ordinary least squares model aligns to the underlying data.

ggplot(df, aes(x, y)) +
  geom_smooth(method = "lm", se = FALSE, lty = "dashed", size = 0.75) +
  geom_point(size = 2) +
  ggtitle("Ordinary least squares model")

Learning parameters with gradient descent

Our quest is to learn this linear relationship using gradient descent. Recall that learning based on gradient descent includes the following steps ℹ️:

  1. Perform a forward pass to compute predicted values,
  2. Compute the loss of our predictions,
  3. Compute the gradient,
  4. Update our weights,
  5. Repeat until loss is sufficiently minimized.

Forward pass

First, we start with some randomized values for our bias (intercept) and slope (weight).

(bias <- runif(1))
[1] 0.9630242
(weight <- runif(1))
[1] 0.902299

This notebook demonstrates stochastic gradient descent which means we are using a batch size of 1. Later notebooks will demo batch sizes > 1. For each step that we take we’ll build a helper function. This first function simply grabs the batch (aka row) of interest.

get_batch <- function(.data, batch_id) {
  .data %>% 
    slice(batch_id)
}

df %>% get_batch(batch_id = 1)

The next step is to make a prediction with our randomized weights:

make_prediction <- function(.data, bias, weight) {
  .data %>% mutate(pred = bias + weight * x)
}

df %>%
  get_batch(batch_id = 1) %>%
  make_prediction(bias, weight)

Now we can compute the error of this prediction which is just the squared error:

compute_sq_error <- function(.data, actual, predicted) {
  .data %>% mutate(error = ({{actual}} - {{predicted}})^2)
}

df %>%
  get_batch(batch_id = 1) %>%
  make_prediction(bias, weight) %>%
  compute_sq_error(y, pred)

Backward pass

Now that we have our error, we can use this information to update our weights. To do so, we need to compute the derivative which we can use to update the weights.

Compute the derivative

There are two ways to compute the derivative. The first is more intuitive and is known as finite differencing. This provides an estimate of the derivative by simply:

  1. adding a small amount to the weight and bias,
  2. making a new prediction with these adjusted amounts,
  3. computing the new error,
  4. and using this information to compute the derivative

The following makes two new predictions; one where we add a small amount to our weight and another where we add a small amount to our bias. We then compute the error of these predictions.

We can see that by increasing the weight and the bias our error gets smaller.

Now we can compute the derivative. Recall that the derivative is just how much y changes divided by how x changes. Here, we make a small adjustment of 0.01 to our bias and weight and in both cases, this adjustment causes a small decrease to our error.

compute_est_derivatives<- function(.data, bias, weight, adj_amt) {
  .data %>% 
    # first compute difference in errors with adjusted bias and weight values
    mutate(
      pred_w_adj_bias = (bias + adj_amt) + weight * x,
      pred_w_adj_weight = bias + (weight + adj_amt) * x,
      error_adj_bias = (y - pred_w_adj_bias)^2,
      error_adj_weight = (y - pred_w_adj_weight)^2
      ) %>%
    # now compute estimated derivative based on adjust predictions
    mutate(
      est_deriv_bias = (error_adj_bias - error) / adj_amt,
      est_deriv_wt = (error_adj_weight - error) / adj_amt,
    ) %>%
    select(-contains("pred_w_adj"))
}
  
df %>%
  get_batch(batch_id = 1) %>%
  make_prediction(bias, weight) %>%
  compute_sq_error(y, pred) %>%
  compute_est_derivatives(bias, weight, adj_amt = 0.01)

The other approach to computing derivatives is to actually compute the derivatives and integrals. With this example, we can do this easily because our underlying function is not complicated.

Considering our underlying function is \(y = bias + 2x\) then the:

  • first derivitive = \(\frac{\delta \text{ error}}{\delta \text{ weight}} = 2 * (\hat{y} - y)\)
  • second derivitive = \(\frac{de}{da} = x * \frac{\delta \text{ error}}{\delta \text{ weight}}\)

There are some R packages that provide auto-differentiation; however, you will never need to manually compute derivatives so we’ll just go off our a priori knowledge of the underlying function. Yes this is cheating a bit but right now we’re just trying to gain intuition for the gradient descent process.

We can see that the estimated derivatives (est_deriv_bias & est_deriv_wt) are good approximates for the actual derived derivatives (deriv_bias & deriv_wt).

compute_derivatives <- function(.data, predicted, actual, x_values) {
  .data %>% 
    mutate(
      deriv_bias = 2 * ({{predicted}} - {{actual}}),
      deriv_wt = deriv_bias * {{x_values}}
    )
}

df %>%
  get_batch(batch_id = 1) %>%
  make_prediction(bias, weight) %>%
  compute_sq_error(y, pred) %>%
  compute_est_derivatives(bias, weight, adj_amt = .01) %>%
  compute_derivatives(pred, y, x)

Updating our weights

To update our model we first need to decide how large of a step we want to take down the gradient descent.This is called the learning rate. New values for our bias and weights are simply the original values minus the derivative times the learning rate.

Here, we use a learning rate of 0.0001. You can see that both the updated bias and weight (new_bias = 0.975 & new_wt = 1.25) have increased in a positive direction compared to the original values (original bias = 0.96 & original weight = 0.90).

update_value <- function(current_value, derivative, learning_rate) {
  current_value - (derivative * learning_rate)
}

df %>%
  get_batch(batch_id = 1) %>%
  make_prediction(bias, weight) %>%
  compute_sq_error(y, pred) %>%
  compute_est_derivatives(bias, weight, adj_amt = .01) %>%
  compute_derivatives(pred, y, x) %>%
  mutate(new_bias = update_value(bias, deriv_bias, 0.0001),
         new_wt = update_value(weight, deriv_wt, 0.0001))

Cycling through an epoch

Recall that after we update our weights, we execute a new batch following the same procedure. We do this for every batch until we have worked through the entire data set and that is considered one epoch. Let’s do this procedure for our data.

First, let’s bring all the steps we just went through together into a single function that does a forward and backward pass on a single batch.

compute_batch_update <- function(.data, bias, weight, learning_rate, batch) {
  .data %>%
    get_batch(batch_id = batch) %>%
    make_prediction(bias, weight) %>%
    compute_sq_error(y, pred) %>%
    compute_est_derivatives(bias, weight, adj_amt = .01) %>%
    compute_derivatives(pred, y, x) %>%
    mutate(new_bias = update_value(bias, deriv_bias, learning_rate),
           new_wt = update_value(weight, deriv_wt, learning_rate))
}
compute_batch_update(df, initial_bias, initial_weight, learning_rate = 0.0001, batch = 1)

We can now create a function that iterates through each batch for the entire data set (1 epoch):

train_epoch <- function(.data, bias, weight, learning_rate) {
  epoch_results <- data.frame()
  for (batch in seq_len(nrow(.data))) {
    batch_results <- compute_batch_update(.data, bias, weight, learning_rate, batch)
    epoch_results <- rbind(epoch_results, batch_results)
    bias <- batch_results[["new_bias"]]
    weight <- batch_results[["new_wt"]]
    }
  epoch_results
  }

If we apply this function we’ll see that the output is our original data frame with the deriviatives and updated bias and weights computed based on each observation.

train_epoch(df, bias, weight, learning_rate = 0.0001)

Cycling through multiple epochs

Now let’s create one last function wich will apply our functions for multiple epochs. This function also provides a new data frame that shows our loss score (MSE) for each epoch along with the original and updated biases & weights.

train_model <- function(.data, bias, weight, learning_rate, epochs) {
  results <- data.frame()
  for (epoch in seq_len(epochs)) {
    epoch_results <- train_epoch(.data, bias, weight, learning_rate) %>%
      mutate(
        bias = bias, 
        weight = weight,
        epoch = epoch,
        mse = sqrt(sum(error))
        ) %>%
      slice(n()) %>%
      select(epoch, bias, weight, mse, new_bias, new_weight = new_wt)
    
    results <- rbind(results, epoch_results)
    
    bias <- epoch_results[["new_bias"]]
    weight <- epoch_results[["new_weight"]]
  }
  results
}

Since we are working with a small learning rate and SGD does not include any momentum, we’ll train this over many epochs. Looking at the last 10 rows in our output we see that the new_bias and new_weight nearly equal the underlying bias (30) and weight (2) that our data was generated with.

history <- train_model(df, bias, weight, learning_rate = 0.0001, epochs = 4000)
tail(history, 10)

The following plot shows the progression down the loss learning curve for each epoch; practically reach 0 towards the end.

ggplot(history, aes(epoch, mse)) +
  geom_point()

The following grabs the results from just some of our epochs (epoch 100, 200, …, 4000) and plots the predicted values based on the estimated bias and weight parameters. You can see how the final estimated bias and weight parameters converge to the same prediction plot illustrated earlier using ordinary least squares.

subset_history <- history %>% filter((epoch %% 100) == 0)

prediction_change <- data.frame()

for (row in seq_len(nrow(subset_history))) {
  bias <- subset_history[[row, "new_bias"]]
  weight <- subset_history[[row, "new_weight"]]
  epoch <- subset_history[[row, "epoch"]]
  y_hat <- make_prediction(df, bias, weight) %>%
    mutate(epoch = epoch)
  prediction_change <- rbind(prediction_change, y_hat)
}

ggplot(prediction_change, aes(x, y)) +
  geom_line(aes(y = pred, group = factor(epoch)), alpha = 0.3) +
  geom_point(color = "red")

LS0tCnRpdGxlOiAiTGluZWFyIHJlZ3Jlc3Npb24gd2l0aCBzdG9jaGFzdGljIGdyYWRpZW50IGRlc2NlbnQiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpCmdncGxvdDI6OnRoZW1lX3NldChnZ3Bsb3QyOjp0aGVtZV9taW5pbWFsKCkpCmBgYAoKVGhpcyBub3RlYm9vayBpbGx1c3RyYXRlcyBob3cgb25lIGNhbiBsZWFybiBwYXJhbWV0ZXJzIHVzaW5nIHN0b2NoYXN0aWMKZ3JhZGllbnQgZGVzY2VudC4gVGhpcyB3aWxsIGRlbW9uc3RyYXRlIHRoZSBiYXNpYyBpZGVhIG9mIGhvdyBpdGVyYXRpbmcgb3Zlcgpmb3J3YXJkIGFuZCBiYWNrd2FyZCBwYXNzZXMgaW1wcm92ZXMgb3VyIGxvc3MgZnVuY3Rpb24uCgpBIGNvdXBsZSB0aGluZ3MgdG8gbm90ZS4gRmlyc3QsIHRoaXMgaXMgbm90ZSBidWlsZGluZyBhIG5ldXJhbCBuZXQgZnJvbSBzY3JhdGNoOwpyYXRoZXIsIGl0IGlzIHVzaW5nIHN0b2NoYXN0aSBncmFkaWVudCBkZXNjZW50IHRvIGJ1aWxkIGEgc2ltcGxlIGxpbmVhcgpyZWdyZXNzaW9uIG1vZGVsIHdoaWNoIHdvdWxkIGJlIGFuYWxvZ291cyB0byBidWlsZGluZyBhIHNpbmdsZSBwZXJjZXB0cm9uLgpTZWNvbmQsIHRoaXMgbm90ZWJvb2sgZG9lcyBub3QgZm9jdXMgb24gZWZmaWNpZW50IGltcGxlbWVudGF0aW9uIG9mIHRoaXMKYWxnb3JpdGhtOyByYXRoZXIsIHRoZSBjb2RlIGlzIGRlc2lnbmVkIHRvIG1ha2UgdGhlIGNvbmNlcHQgaW50dWl0aXZlIHRvIHRoZQpyZWFkZXIuCgojIFJlcXVpcmVtZW50cwoKYGBge3IsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9CmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KGdsdWUpCmBgYAoKIyBDcmVhdGluZyBvdXIgZGF0YQoKRm9yIHNpbXBsaWNpdHkgd2UgYXJlIGdvaW5nIHRvIHVzZSBhIHZlcnkgc2ltcGxlIGRhdGEgc2V0IHRoYXQgaXMgZ2VuZXJhdGVkCmJhc2VkIG9uIGEgc2ltcGxlIGxpbmVhciBtb2RlbCB3aXRob3V0IGFueSBlcnJvci4gT3VyIGRhdGEgaW5jbHVkZXM6CgotIDMwIG9ic2VydmF0aW9ucyAobiksCi0gd2l0aCBhbiBpbnRlcmNlcHQgb2YgMiAoYiksCi0gYW5kIHNsb3BlIG9mIDMwIChhKQoKYGBge3J9Cm4gPC0gMzAKYiA8LSAzMAphIDwtIDIKCnNldC5zZWVkKDEyMykKKGRmIDwtIHRpYmJsZSgKICB4ID0gcnVuaWYoMzAsIDAsIDEwMCksIAogIHkgPSBiICsgYSp4CiAgKSkKYGBgCgpUaGUgZm9sbG93aW5nIHBsb3Qgc2hvd3MgaG93IGFuIG9yZGluYXJ5IGxlYXN0IHNxdWFyZXMgbW9kZWwgYWxpZ25zIHRvIHRoZQp1bmRlcmx5aW5nIGRhdGEuCgpgYGB7cn0KZ2dwbG90KGRmLCBhZXMoeCwgeSkpICsKICBnZW9tX3Ntb290aChtZXRob2QgPSAibG0iLCBzZSA9IEZBTFNFLCBsdHkgPSAiZGFzaGVkIiwgc2l6ZSA9IDAuNzUpICsKICBnZW9tX3BvaW50KHNpemUgPSAyKSArCiAgZ2d0aXRsZSgiT3JkaW5hcnkgbGVhc3Qgc3F1YXJlcyBtb2RlbCIpCmBgYAoKCiMgTGVhcm5pbmcgcGFyYW1ldGVycyB3aXRoIGdyYWRpZW50IGRlc2NlbnQKCk91ciBxdWVzdCBpcyB0byBsZWFybiB0aGlzIGxpbmVhciByZWxhdGlvbnNoaXAgdXNpbmcgZ3JhZGllbnQgZGVzY2VudC4gUmVjYWxsCnRoYXQgbGVhcm5pbmcgYmFzZWQgb24gZ3JhZGllbnQgZGVzY2VudCBpbmNsdWRlcyB0aGUgZm9sbG93aW5nIHN0ZXBzClvihLnvuI9dKGh0dHA6Ly9iaXQubHkvZGwtMDEjNDkpOgoKMS4gUGVyZm9ybSBhIGZvcndhcmQgcGFzcyB0byBjb21wdXRlIHByZWRpY3RlZCB2YWx1ZXMsCjIuIENvbXB1dGUgdGhlIGxvc3Mgb2Ygb3VyIHByZWRpY3Rpb25zLAozLiBDb21wdXRlIHRoZSBncmFkaWVudCwKNC4gVXBkYXRlIG91ciB3ZWlnaHRzLAo1LiBSZXBlYXQgdW50aWwgbG9zcyBpcyBzdWZmaWNpZW50bHkgbWluaW1pemVkLgoKIyMgRm9yd2FyZCBwYXNzCgpGaXJzdCwgd2Ugc3RhcnQgd2l0aCBzb21lIHJhbmRvbWl6ZWQgdmFsdWVzIGZvciBvdXIgYmlhcyAoaW50ZXJjZXB0KSBhbmQgc2xvcGUKKHdlaWdodCkuIAoKYGBge3J9CihiaWFzIDwtIHJ1bmlmKDEpKQood2VpZ2h0IDwtIHJ1bmlmKDEpKQpgYGAKClRoaXMgbm90ZWJvb2sgZGVtb25zdHJhdGVzIHN0b2NoYXN0aWMgZ3JhZGllbnQgZGVzY2VudCB3aGljaCBtZWFucyB3ZSBhcmUgdXNpbmcKYSBiYXRjaCBzaXplIG9mIDEuIExhdGVyIG5vdGVib29rcyB3aWxsIGRlbW8gYmF0Y2ggc2l6ZXMgPiAxLiBGb3IgZWFjaCBzdGVwIHRoYXQKd2UgdGFrZSB3ZSdsbCBidWlsZCBhIGhlbHBlciBmdW5jdGlvbi4gVGhpcyBmaXJzdCBmdW5jdGlvbiBzaW1wbHkgZ3JhYnMgdGhlCmJhdGNoIChha2Egcm93KSBvZiBpbnRlcmVzdC4KCmBgYHtyfQpnZXRfYmF0Y2ggPC0gZnVuY3Rpb24oLmRhdGEsIGJhdGNoX2lkKSB7CiAgLmRhdGEgJT4lIAogICAgc2xpY2UoYmF0Y2hfaWQpCn0KCmRmICU+JSBnZXRfYmF0Y2goYmF0Y2hfaWQgPSAxKQpgYGAKClRoZSBuZXh0IHN0ZXAgaXMgdG8gbWFrZSBhIHByZWRpY3Rpb24gd2l0aCBvdXIgcmFuZG9taXplZCB3ZWlnaHRzOgoKYGBge3J9Cm1ha2VfcHJlZGljdGlvbiA8LSBmdW5jdGlvbiguZGF0YSwgYmlhcywgd2VpZ2h0KSB7CiAgLmRhdGEgJT4lIG11dGF0ZShwcmVkID0gYmlhcyArIHdlaWdodCAqIHgpCn0KCmRmICU+JQogIGdldF9iYXRjaChiYXRjaF9pZCA9IDEpICU+JQogIG1ha2VfcHJlZGljdGlvbihiaWFzLCB3ZWlnaHQpCmBgYAoKTm93IHdlIGNhbiBjb21wdXRlIHRoZSBlcnJvciBvZiB0aGlzIHByZWRpY3Rpb24gd2hpY2ggaXMganVzdCB0aGUgc3F1YXJlZCBlcnJvcjoKCmBgYHtyfQpjb21wdXRlX3NxX2Vycm9yIDwtIGZ1bmN0aW9uKC5kYXRhLCBhY3R1YWwsIHByZWRpY3RlZCkgewogIC5kYXRhICU+JSBtdXRhdGUoZXJyb3IgPSAoe3thY3R1YWx9fSAtIHt7cHJlZGljdGVkfX0pXjIpCn0KCmRmICU+JQogIGdldF9iYXRjaChiYXRjaF9pZCA9IDEpICU+JQogIG1ha2VfcHJlZGljdGlvbihiaWFzLCB3ZWlnaHQpICU+JQogIGNvbXB1dGVfc3FfZXJyb3IoeSwgcHJlZCkKYGBgCgojIyBCYWNrd2FyZCBwYXNzCgpOb3cgdGhhdCB3ZSBoYXZlIG91ciBlcnJvciwgd2UgY2FuIHVzZSB0aGlzIGluZm9ybWF0aW9uIHRvIHVwZGF0ZSBvdXIgd2VpZ2h0cy4KVG8gZG8gc28sIHdlIG5lZWQgdG8gY29tcHV0ZSB0aGUgZGVyaXZhdGl2ZSB3aGljaCB3ZSBjYW4gdXNlIHRvIHVwZGF0ZSB0aGUKd2VpZ2h0cy4KCiMjIyBDb21wdXRlIHRoZSBkZXJpdmF0aXZlCgpUaGVyZSBhcmUgdHdvIHdheXMgdG8gY29tcHV0ZSB0aGUgZGVyaXZhdGl2ZS4gVGhlIGZpcnN0IGlzIG1vcmUgaW50dWl0aXZlIGFuZCBpcwprbm93biBhcyBmaW5pdGUgZGlmZmVyZW5jaW5nLiBUaGlzIHByb3ZpZGVzIGFuIF9lc3RpbWF0ZV8gb2YgdGhlIGRlcml2YXRpdmUgYnkKc2ltcGx5OgoKMS4gYWRkaW5nIGEgc21hbGwgYW1vdW50IHRvIHRoZSB3ZWlnaHQgYW5kIGJpYXMsIAoyLiBtYWtpbmcgYSBuZXcgcHJlZGljdGlvbiB3aXRoIHRoZXNlIGFkanVzdGVkIGFtb3VudHMsCjMuIGNvbXB1dGluZyB0aGUgbmV3IGVycm9yLAo0LiBhbmQgdXNpbmcgdGhpcyBpbmZvcm1hdGlvbiB0byBjb21wdXRlIHRoZSBkZXJpdmF0aXZlCgpUaGUgZm9sbG93aW5nIG1ha2VzIHR3byBuZXcgcHJlZGljdGlvbnM7IG9uZSB3aGVyZSB3ZSBhZGQgYSBzbWFsbCBhbW91bnQgdG8gb3VyCndlaWdodCBhbmQgYW5vdGhlciB3aGVyZSB3ZSBhZGQgYSBzbWFsbCBhbW91bnQgdG8gb3VyIGJpYXMuIFdlIHRoZW4gY29tcHV0ZSB0aGUKZXJyb3Igb2YgdGhlc2UgcHJlZGljdGlvbnMuCgpXZSBjYW4gc2VlIHRoYXQgYnkgaW5jcmVhc2luZyB0aGUgd2VpZ2h0IGFuZCB0aGUgYmlhcyBvdXIgZXJyb3IgZ2V0cyBzbWFsbGVyLgoKTm93IHdlIGNhbiBjb21wdXRlIHRoZSBkZXJpdmF0aXZlLiBSZWNhbGwgdGhhdCB0aGUgZGVyaXZhdGl2ZSBpcyBqdXN0IGhvdyBtdWNoCnkgY2hhbmdlcyBkaXZpZGVkIGJ5IGhvdyB4IGNoYW5nZXMuIEhlcmUsIHdlIG1ha2UgYSBzbWFsbCBhZGp1c3RtZW50IG9mIDAuMDEgdG8Kb3VyIGJpYXMgYW5kIHdlaWdodCBhbmQgaW4gYm90aCBjYXNlcywgdGhpcyBhZGp1c3RtZW50IGNhdXNlcyBhIHNtYWxsIGRlY3JlYXNlCnRvIG91ciBlcnJvci4KCmBgYHtyfQpjb21wdXRlX2VzdF9kZXJpdmF0aXZlczwtIGZ1bmN0aW9uKC5kYXRhLCBiaWFzLCB3ZWlnaHQsIGFkal9hbXQpIHsKICAuZGF0YSAlPiUgCiAgICAjIGZpcnN0IGNvbXB1dGUgZGlmZmVyZW5jZSBpbiBlcnJvcnMgd2l0aCBhZGp1c3RlZCBiaWFzIGFuZCB3ZWlnaHQgdmFsdWVzCiAgICBtdXRhdGUoCiAgICAgIHByZWRfd19hZGpfYmlhcyA9IChiaWFzICsgYWRqX2FtdCkgKyB3ZWlnaHQgKiB4LAogICAgICBwcmVkX3dfYWRqX3dlaWdodCA9IGJpYXMgKyAod2VpZ2h0ICsgYWRqX2FtdCkgKiB4LAogICAgICBlcnJvcl9hZGpfYmlhcyA9ICh5IC0gcHJlZF93X2Fkal9iaWFzKV4yLAogICAgICBlcnJvcl9hZGpfd2VpZ2h0ID0gKHkgLSBwcmVkX3dfYWRqX3dlaWdodCleMgogICAgICApICU+JQogICAgIyBub3cgY29tcHV0ZSBlc3RpbWF0ZWQgZGVyaXZhdGl2ZSBiYXNlZCBvbiBhZGp1c3QgcHJlZGljdGlvbnMKICAgIG11dGF0ZSgKICAgICAgZXN0X2Rlcml2X2JpYXMgPSAoZXJyb3JfYWRqX2JpYXMgLSBlcnJvcikgLyBhZGpfYW10LAogICAgICBlc3RfZGVyaXZfd3QgPSAoZXJyb3JfYWRqX3dlaWdodCAtIGVycm9yKSAvIGFkal9hbXQsCiAgICApICU+JQogICAgc2VsZWN0KC1jb250YWlucygicHJlZF93X2FkaiIpKQp9CiAgCmRmICU+JQogIGdldF9iYXRjaChiYXRjaF9pZCA9IDEpICU+JQogIG1ha2VfcHJlZGljdGlvbihiaWFzLCB3ZWlnaHQpICU+JQogIGNvbXB1dGVfc3FfZXJyb3IoeSwgcHJlZCkgJT4lCiAgY29tcHV0ZV9lc3RfZGVyaXZhdGl2ZXMoYmlhcywgd2VpZ2h0LCBhZGpfYW10ID0gMC4wMSkKYGBgCgpUaGUgb3RoZXIgYXBwcm9hY2ggdG8gY29tcHV0aW5nIGRlcml2YXRpdmVzIGlzIHRvIGFjdHVhbGx5IGNvbXB1dGUgdGhlCmRlcml2YXRpdmVzIGFuZCBpbnRlZ3JhbHMuIFdpdGggdGhpcyBleGFtcGxlLCB3ZSBjYW4gZG8gdGhpcyBlYXNpbHkgYmVjYXVzZSBvdXIKdW5kZXJseWluZyBmdW5jdGlvbiBpcyBub3QgY29tcGxpY2F0ZWQuCgpDb25zaWRlcmluZyBvdXIgdW5kZXJseWluZyBmdW5jdGlvbiBpcyAkeSA9IGJpYXMgKyAyeCQgdGhlbiB0aGU6CgoqIGZpcnN0IGRlcml2aXRpdmUgPSAkXGZyYWN7XGRlbHRhIFx0ZXh0eyBlcnJvcn19e1xkZWx0YSBcdGV4dHsgd2VpZ2h0fX0gPSAyICogKFxoYXR7eX0gLSB5KSQKKiBzZWNvbmQgZGVyaXZpdGl2ZSA9ICRcZnJhY3tkZX17ZGF9ID0geCAqIFxmcmFje1xkZWx0YSBcdGV4dHsgZXJyb3J9fXtcZGVsdGEgXHRleHR7IHdlaWdodH19JAoKVGhlcmUgYXJlIHNvbWUgUiBwYWNrYWdlcyB0aGF0IHByb3ZpZGUgYXV0by1kaWZmZXJlbnRpYXRpb247IGhvd2V2ZXIsIHlvdSB3aWxsCm5ldmVyIG5lZWQgdG8gbWFudWFsbHkgY29tcHV0ZSBkZXJpdmF0aXZlcyBzbyB3ZSdsbCBqdXN0IGdvIG9mZiBvdXIgYSBwcmlvcmkKa25vd2xlZGdlIG9mIHRoZSB1bmRlcmx5aW5nIGZ1bmN0aW9uLiBZZXMgdGhpcyBpcyBjaGVhdGluZyBhIGJpdCBidXQgcmlnaHQgbm93CndlJ3JlIGp1c3QgdHJ5aW5nIHRvIGdhaW4gaW50dWl0aW9uIGZvciB0aGUgZ3JhZGllbnQgZGVzY2VudCBwcm9jZXNzLgoKV2UgY2FuIHNlZSB0aGF0IHRoZSBlc3RpbWF0ZWQgZGVyaXZhdGl2ZXMgKGBlc3RfZGVyaXZfYmlhc2AgJiBgZXN0X2Rlcml2X3d0YCkKYXJlIGdvb2QgYXBwcm94aW1hdGVzIGZvciB0aGUgYWN0dWFsIGRlcml2ZWQgZGVyaXZhdGl2ZXMgKGBkZXJpdl9iaWFzYCAmCmBkZXJpdl93dGApLgoKYGBge3J9CmNvbXB1dGVfZGVyaXZhdGl2ZXMgPC0gZnVuY3Rpb24oLmRhdGEsIHByZWRpY3RlZCwgYWN0dWFsLCB4X3ZhbHVlcykgewogIC5kYXRhICU+JSAKICAgIG11dGF0ZSgKICAgICAgZGVyaXZfYmlhcyA9IDIgKiAoe3twcmVkaWN0ZWR9fSAtIHt7YWN0dWFsfX0pLAogICAgICBkZXJpdl93dCA9IGRlcml2X2JpYXMgKiB7e3hfdmFsdWVzfX0KICAgICkKfQoKZGYgJT4lCiAgZ2V0X2JhdGNoKGJhdGNoX2lkID0gMSkgJT4lCiAgbWFrZV9wcmVkaWN0aW9uKGJpYXMsIHdlaWdodCkgJT4lCiAgY29tcHV0ZV9zcV9lcnJvcih5LCBwcmVkKSAlPiUKICBjb21wdXRlX2VzdF9kZXJpdmF0aXZlcyhiaWFzLCB3ZWlnaHQsIGFkal9hbXQgPSAuMDEpICU+JQogIGNvbXB1dGVfZGVyaXZhdGl2ZXMocHJlZCwgeSwgeCkKYGBgCgojIyMgVXBkYXRpbmcgb3VyIHdlaWdodHMKClRvIHVwZGF0ZSBvdXIgbW9kZWwgd2UgZmlyc3QgbmVlZCB0byBkZWNpZGUgaG93IGxhcmdlIG9mIGEgc3RlcCB3ZSB3YW50IHRvIHRha2UKZG93biB0aGUgZ3JhZGllbnQgZGVzY2VudC5UaGlzIGlzIGNhbGxlZCB0aGUgbGVhcm5pbmcgcmF0ZS4gTmV3IHZhbHVlcyBmb3Igb3VyCmJpYXMgYW5kIHdlaWdodHMgYXJlIHNpbXBseSB0aGUgb3JpZ2luYWwgdmFsdWVzIG1pbnVzIHRoZSBkZXJpdmF0aXZlIHRpbWVzIHRoZQpsZWFybmluZyByYXRlLiAKCkhlcmUsIHdlIHVzZSBhIGxlYXJuaW5nIHJhdGUgb2YgYDAuMDAwMWAuIFlvdSBjYW4gc2VlIHRoYXQgYm90aCB0aGUgdXBkYXRlZCBiaWFzCmFuZCB3ZWlnaHQgKGBuZXdfYmlhcyA9IDAuOTc1YCAmIGBuZXdfd3QgPSAxLjI1YCkgaGF2ZSBpbmNyZWFzZWQgaW4gYSBwb3NpdGl2ZQpkaXJlY3Rpb24gY29tcGFyZWQgdG8gdGhlIG9yaWdpbmFsIHZhbHVlcyAob3JpZ2luYWwgYmlhcyA9IDAuOTYgJiBvcmlnaW5hbAp3ZWlnaHQgPSAwLjkwKS4KCmBgYHtyfQp1cGRhdGVfdmFsdWUgPC0gZnVuY3Rpb24oY3VycmVudF92YWx1ZSwgZGVyaXZhdGl2ZSwgbGVhcm5pbmdfcmF0ZSkgewogIGN1cnJlbnRfdmFsdWUgLSAoZGVyaXZhdGl2ZSAqIGxlYXJuaW5nX3JhdGUpCn0KCmRmICU+JQogIGdldF9iYXRjaChiYXRjaF9pZCA9IDEpICU+JQogIG1ha2VfcHJlZGljdGlvbihiaWFzLCB3ZWlnaHQpICU+JQogIGNvbXB1dGVfc3FfZXJyb3IoeSwgcHJlZCkgJT4lCiAgY29tcHV0ZV9lc3RfZGVyaXZhdGl2ZXMoYmlhcywgd2VpZ2h0LCBhZGpfYW10ID0gLjAxKSAlPiUKICBjb21wdXRlX2Rlcml2YXRpdmVzKHByZWQsIHksIHgpICU+JQogIG11dGF0ZShuZXdfYmlhcyA9IHVwZGF0ZV92YWx1ZShiaWFzLCBkZXJpdl9iaWFzLCAwLjAwMDEpLAogICAgICAgICBuZXdfd3QgPSB1cGRhdGVfdmFsdWUod2VpZ2h0LCBkZXJpdl93dCwgMC4wMDAxKSkKYGBgCgojIyBDeWNsaW5nIHRocm91Z2ggYW4gZXBvY2gKClJlY2FsbCB0aGF0IGFmdGVyIHdlIHVwZGF0ZSBvdXIgd2VpZ2h0cywgd2UgZXhlY3V0ZSBhIG5ldyBiYXRjaCBmb2xsb3dpbmcgdGhlCnNhbWUgcHJvY2VkdXJlLiBXZSBkbyB0aGlzIGZvciBldmVyeSBiYXRjaCB1bnRpbCB3ZSBoYXZlIHdvcmtlZCB0aHJvdWdoIHRoZQplbnRpcmUgZGF0YSBzZXQgYW5kIHRoYXQgaXMgY29uc2lkZXJlZCBvbmUgZXBvY2guIExldCdzIGRvIHRoaXMgcHJvY2VkdXJlIGZvcgpvdXIgZGF0YS4gCgpGaXJzdCwgbGV0J3MgYnJpbmcgYWxsIHRoZSBzdGVwcyB3ZSBqdXN0IHdlbnQgdGhyb3VnaCB0b2dldGhlciBpbnRvIGEgc2luZ2xlCmZ1bmN0aW9uIHRoYXQgZG9lcyBhIGZvcndhcmQgYW5kIGJhY2t3YXJkIHBhc3Mgb24gYSBzaW5nbGUgYmF0Y2guCgpgYGB7cn0KY29tcHV0ZV9iYXRjaF91cGRhdGUgPC0gZnVuY3Rpb24oLmRhdGEsIGJpYXMsIHdlaWdodCwgbGVhcm5pbmdfcmF0ZSwgYmF0Y2gpIHsKICAuZGF0YSAlPiUKICAgIGdldF9iYXRjaChiYXRjaF9pZCA9IGJhdGNoKSAlPiUKICAgIG1ha2VfcHJlZGljdGlvbihiaWFzLCB3ZWlnaHQpICU+JQogICAgY29tcHV0ZV9zcV9lcnJvcih5LCBwcmVkKSAlPiUKICAgIGNvbXB1dGVfZXN0X2Rlcml2YXRpdmVzKGJpYXMsIHdlaWdodCwgYWRqX2FtdCA9IC4wMSkgJT4lCiAgICBjb21wdXRlX2Rlcml2YXRpdmVzKHByZWQsIHksIHgpICU+JQogICAgbXV0YXRlKG5ld19iaWFzID0gdXBkYXRlX3ZhbHVlKGJpYXMsIGRlcml2X2JpYXMsIGxlYXJuaW5nX3JhdGUpLAogICAgICAgICAgIG5ld193dCA9IHVwZGF0ZV92YWx1ZSh3ZWlnaHQsIGRlcml2X3d0LCBsZWFybmluZ19yYXRlKSkKfQpgYGAKCgpgYGB7cn0KY29tcHV0ZV9iYXRjaF91cGRhdGUoZGYsIGluaXRpYWxfYmlhcywgaW5pdGlhbF93ZWlnaHQsIGxlYXJuaW5nX3JhdGUgPSAwLjAwMDEsIGJhdGNoID0gMSkKYGBgCgpXZSBjYW4gbm93IGNyZWF0ZSBhIGZ1bmN0aW9uIHRoYXQgaXRlcmF0ZXMgdGhyb3VnaCBlYWNoIGJhdGNoIGZvciB0aGUgZW50aXJlCmRhdGEgc2V0ICgxIGVwb2NoKToKCmBgYHtyfQp0cmFpbl9lcG9jaCA8LSBmdW5jdGlvbiguZGF0YSwgYmlhcywgd2VpZ2h0LCBsZWFybmluZ19yYXRlKSB7CiAgZXBvY2hfcmVzdWx0cyA8LSBkYXRhLmZyYW1lKCkKICBmb3IgKGJhdGNoIGluIHNlcV9sZW4obnJvdyguZGF0YSkpKSB7CiAgICBiYXRjaF9yZXN1bHRzIDwtIGNvbXB1dGVfYmF0Y2hfdXBkYXRlKC5kYXRhLCBiaWFzLCB3ZWlnaHQsIGxlYXJuaW5nX3JhdGUsIGJhdGNoKQogICAgZXBvY2hfcmVzdWx0cyA8LSByYmluZChlcG9jaF9yZXN1bHRzLCBiYXRjaF9yZXN1bHRzKQogICAgYmlhcyA8LSBiYXRjaF9yZXN1bHRzW1sibmV3X2JpYXMiXV0KICAgIHdlaWdodCA8LSBiYXRjaF9yZXN1bHRzW1sibmV3X3d0Il1dCiAgICB9CiAgZXBvY2hfcmVzdWx0cwogIH0KYGBgCgpJZiB3ZSBhcHBseSB0aGlzIGZ1bmN0aW9uIHdlJ2xsIHNlZSB0aGF0IHRoZSBvdXRwdXQgaXMgb3VyIG9yaWdpbmFsIGRhdGEgZnJhbWUKd2l0aCB0aGUgZGVyaXZpYXRpdmVzIGFuZCB1cGRhdGVkIGJpYXMgYW5kIHdlaWdodHMgY29tcHV0ZWQgYmFzZWQgb24gZWFjaApvYnNlcnZhdGlvbi4gCgpgYGB7cn0KdHJhaW5fZXBvY2goZGYsIGJpYXMsIHdlaWdodCwgbGVhcm5pbmdfcmF0ZSA9IDAuMDAwMSkKYGBgCgojIyBDeWNsaW5nIHRocm91Z2ggbXVsdGlwbGUgZXBvY2hzCgpOb3cgbGV0J3MgY3JlYXRlIG9uZSBsYXN0IGZ1bmN0aW9uIHdpY2ggd2lsbCBhcHBseSBvdXIgZnVuY3Rpb25zIGZvciBtdWx0aXBsZQplcG9jaHMuIFRoaXMgZnVuY3Rpb24gYWxzbyBwcm92aWRlcyBhIG5ldyBkYXRhIGZyYW1lIHRoYXQgc2hvd3Mgb3VyIGxvc3Mgc2NvcmUKKE1TRSkgZm9yIGVhY2ggZXBvY2ggYWxvbmcgd2l0aCB0aGUgb3JpZ2luYWwgYW5kIHVwZGF0ZWQgYmlhc2VzICYgd2VpZ2h0cy4KCmBgYHtyfQp0cmFpbl9tb2RlbCA8LSBmdW5jdGlvbiguZGF0YSwgYmlhcywgd2VpZ2h0LCBsZWFybmluZ19yYXRlLCBlcG9jaHMpIHsKICByZXN1bHRzIDwtIGRhdGEuZnJhbWUoKQogIGZvciAoZXBvY2ggaW4gc2VxX2xlbihlcG9jaHMpKSB7CiAgICBlcG9jaF9yZXN1bHRzIDwtIHRyYWluX2Vwb2NoKC5kYXRhLCBiaWFzLCB3ZWlnaHQsIGxlYXJuaW5nX3JhdGUpICU+JQogICAgICBtdXRhdGUoCiAgICAgICAgYmlhcyA9IGJpYXMsIAogICAgICAgIHdlaWdodCA9IHdlaWdodCwKICAgICAgICBlcG9jaCA9IGVwb2NoLAogICAgICAgIG1zZSA9IHNxcnQoc3VtKGVycm9yKSkKICAgICAgICApICU+JQogICAgICBzbGljZShuKCkpICU+JQogICAgICBzZWxlY3QoZXBvY2gsIGJpYXMsIHdlaWdodCwgbXNlLCBuZXdfYmlhcywgbmV3X3dlaWdodCA9IG5ld193dCkKICAgIAogICAgcmVzdWx0cyA8LSByYmluZChyZXN1bHRzLCBlcG9jaF9yZXN1bHRzKQogICAgCiAgICBiaWFzIDwtIGVwb2NoX3Jlc3VsdHNbWyJuZXdfYmlhcyJdXQogICAgd2VpZ2h0IDwtIGVwb2NoX3Jlc3VsdHNbWyJuZXdfd2VpZ2h0Il1dCiAgfQogIHJlc3VsdHMKfQpgYGAKClNpbmNlIHdlIGFyZSB3b3JraW5nIHdpdGggYSBzbWFsbCBsZWFybmluZyByYXRlIGFuZCBTR0QgZG9lcyBub3QgaW5jbHVkZSBhbnkKbW9tZW50dW0sIHdlJ2xsIHRyYWluIHRoaXMgb3ZlciBtYW55IGVwb2Nocy4gTG9va2luZyBhdCB0aGUgbGFzdCAxMCByb3dzIGluIG91cgpvdXRwdXQgd2Ugc2VlIHRoYXQgdGhlIGBuZXdfYmlhc2AgYW5kIGBuZXdfd2VpZ2h0YCBuZWFybHkgZXF1YWwgdGhlIHVuZGVybHlpbmcKYmlhcyAoMzApIGFuZCB3ZWlnaHQgKDIpIHRoYXQgb3VyIGRhdGEgd2FzIGdlbmVyYXRlZCB3aXRoLgoKYGBge3J9Cmhpc3RvcnkgPC0gdHJhaW5fbW9kZWwoZGYsIGJpYXMsIHdlaWdodCwgbGVhcm5pbmdfcmF0ZSA9IDAuMDAwMSwgZXBvY2hzID0gNDAwMCkKdGFpbChoaXN0b3J5LCAxMCkKYGBgCgpUaGUgZm9sbG93aW5nIHBsb3Qgc2hvd3MgdGhlIHByb2dyZXNzaW9uIGRvd24gdGhlIGxvc3MgbGVhcm5pbmcgY3VydmUgZm9yIGVhY2gKZXBvY2g7IHByYWN0aWNhbGx5IHJlYWNoIDAgdG93YXJkcyB0aGUgZW5kLgoKYGBge3J9CmdncGxvdChoaXN0b3J5LCBhZXMoZXBvY2gsIG1zZSkpICsKICBnZW9tX3BvaW50KCkKYGBgCgpUaGUgZm9sbG93aW5nIGdyYWJzIHRoZSByZXN1bHRzIGZyb20ganVzdCBzb21lIG9mIG91ciBlcG9jaHMgKGVwb2NoIDEwMCwgMjAwLCAuLi4sCjQwMDApIGFuZCBwbG90cyB0aGUgcHJlZGljdGVkIHZhbHVlcyBiYXNlZCBvbiB0aGUgZXN0aW1hdGVkIGJpYXMgYW5kIHdlaWdodApwYXJhbWV0ZXJzLiBZb3UgY2FuIHNlZSBob3cgdGhlIGZpbmFsIGVzdGltYXRlZCBiaWFzIGFuZCB3ZWlnaHQgcGFyYW1ldGVycwpjb252ZXJnZSB0byB0aGUgc2FtZSBwcmVkaWN0aW9uIHBsb3QgaWxsdXN0cmF0ZWQgZWFybGllciB1c2luZyBvcmRpbmFyeSBsZWFzdApzcXVhcmVzLgoKYGBge3J9CnN1YnNldF9oaXN0b3J5IDwtIGhpc3RvcnkgJT4lIGZpbHRlcigoZXBvY2ggJSUgMTAwKSA9PSAwKQoKcHJlZGljdGlvbl9jaGFuZ2UgPC0gZGF0YS5mcmFtZSgpCgpmb3IgKHJvdyBpbiBzZXFfbGVuKG5yb3coc3Vic2V0X2hpc3RvcnkpKSkgewogIGJpYXMgPC0gc3Vic2V0X2hpc3RvcnlbW3JvdywgIm5ld19iaWFzIl1dCiAgd2VpZ2h0IDwtIHN1YnNldF9oaXN0b3J5W1tyb3csICJuZXdfd2VpZ2h0Il1dCiAgZXBvY2ggPC0gc3Vic2V0X2hpc3RvcnlbW3JvdywgImVwb2NoIl1dCiAgeV9oYXQgPC0gbWFrZV9wcmVkaWN0aW9uKGRmLCBiaWFzLCB3ZWlnaHQpICU+JQogICAgbXV0YXRlKGVwb2NoID0gZXBvY2gpCiAgcHJlZGljdGlvbl9jaGFuZ2UgPC0gcmJpbmQocHJlZGljdGlvbl9jaGFuZ2UsIHlfaGF0KQp9CgpnZ3Bsb3QocHJlZGljdGlvbl9jaGFuZ2UsIGFlcyh4LCB5KSkgKwogIGdlb21fbGluZShhZXMoeSA9IHByZWQsIGdyb3VwID0gZmFjdG9yKGVwb2NoKSksIGFscGhhID0gMC4zKSArCiAgZ2VvbV9wb2ludChjb2xvciA9ICJyZWQiKQpgYGAKCg==