Note: Some results may differ from the hard copy book due to the changing of sampling procedures introduced in R 3.6.0. See http://bit.ly/35D1SW7 for more details. Access and run the source code for this notebook here.

Hidden chapter requirements used in the book to set the plotting theme and load packages used in hidden code chunks:

knitr::opts_chunk$set(
  message = FALSE, 
  warning = FALSE, 
  cache = FALSE
)

# Set the graphical theme
ggplot2::theme_set(ggplot2::theme_light())

Prerequisites

This chapter leverages the following packages:

# Helper packages
library(dplyr)    # for data manipulation
library(ggplot2)  # for data visualization
library(tidyr)    # for data reshaping

# Modeling packages
library(h2o)  # for fitting GLRMs

To illustrate GLRM concepts, we’ll continue using the my_basket data set created in the previous chapter:

url <- "https://koalaverse.github.io/homlr/data/my_basket.csv"
my_basket <- readr::read_csv(url)

The idea

head(mtcars)

Figure 18.1:

knitr::include_graphics("images/glrm-example.png")

Finding the lower ranks

Loss functions

Figure 18.2:

knitr::include_graphics("images/quadratic-huber-loss.png")

Fitting GLRMs in R

h2o.no_progress()  # turn off progress bars
h2o.init(max_mem_size = "5g")  # connect to H2O instance

Basic GLRM model

# convert data to h2o object
my_basket.h2o <- as.h2o(my_basket)

# run basic GLRM
basic_glrm <- h2o.glrm(
  training_frame = my_basket.h2o,
  k = 20, 
  loss = "Quadratic",
  regularization_x = "None", 
  regularization_y = "None", 
  transform = "STANDARDIZE", 
  max_iterations = 2000,
  seed = 123
)
# get top level summary information on our model
summary(basic_glrm)
Model Details:
==============

H2ODimReductionModel: glrm
Model Key:  GLRM_model_R_1577620863288_5 
Model Summary: 

H2ODimReductionMetrics: glrm
** Reported on training data. **

Sum of Squared Error (Numeric):  31004.59
Misclassification Error (Categorical):  0
Number of Numeric Entries:  84000
Number of Categorical Entries:  0



Scoring History: 

---
plot(basic_glrm)

# amount of variance explained by each archetype (aka "pc")
basic_glrm@model$importance
Importance of components: 
data.frame(
    PC  = basic_glrm@model$importance %>% seq_along(),
    PVE = basic_glrm@model$importance %>% .[2,] %>% unlist(),
    CVE = basic_glrm@model$importance %>% .[3,] %>% unlist()
) %>%
    gather(metric, variance_explained, -PC) %>%
    ggplot(aes(PC, variance_explained)) +
    geom_point() +
    facet_wrap(~ metric, ncol = 1, scales = "free")

t(basic_glrm@model$archetypes)[1:5, 1:5]
             Arch1      Arch2      Arch3      Arch4       Arch5
7up     -0.5783538 -1.5705325  0.9906612 -0.9306704  0.17552643
lasagna  0.2196728  0.1213954 -0.7068851  0.8436524  3.56206178
pepsi   -0.2504310 -0.8156136 -0.7669562 -1.2551630 -0.47632696
yop     -0.1856632  0.4000083 -0.4855958  1.1598919 -0.26142763
redwine -0.1372589 -0.1059148 -0.9579530  0.4641668 -0.08539977
p1 <- t(basic_glrm@model$archetypes) %>% 
  as.data.frame() %>% 
  mutate(feature = row.names(.)) %>%
  ggplot(aes(Arch1, reorder(feature, Arch1))) +
  geom_point()

p2 <- t(basic_glrm@model$archetypes) %>% 
  as.data.frame() %>% 
  mutate(feature = row.names(.)) %>%
  ggplot(aes(Arch1, Arch2, label = feature)) +
  geom_text()

gridExtra::grid.arrange(p1, p2, nrow = 1)

# Re-run model with k = 8
k8_glrm <- h2o.glrm(
  training_frame = my_basket.h2o,
  k = 8, 
  loss = "Quadratic",
  regularization_x = "None", 
  regularization_y = "None", 
  transform = "STANDARDIZE", 
  max_iterations = 2000,
  seed = 123
)

# Reconstruct to see how well the model did
my_reconstruction <- h2o.reconstruct(k8_glrm, my_basket.h2o, reverse_transform = TRUE)

# Raw predicted values
my_reconstruction[1:5, 1:5]

[5 rows x 5 columns] 
# Round values to whole integers
my_reconstruction[1:5, 1:5] %>% round(0)

[5 rows x 5 columns] 

Tuning to optimize for unseen data

# Use non-negative regularization
k8_glrm_regularized <- h2o.glrm(
  training_frame = my_basket.h2o,
  k = 8, 
  loss = "Quadratic",
  regularization_x = "NonNegative", 
  regularization_y = "NonNegative",
  gamma_x = 0.5,
  gamma_y = 0.5,
  transform = "STANDARDIZE", 
  max_iterations = 2000,
  seed = 123
)

# Show predicted values
predict(k8_glrm_regularized, my_basket.h2o)[1:5, 1:5]

[5 rows x 5 columns] 
# Compare regularized versus non-regularized loss
par(mfrow = c(1, 2))
plot(k8_glrm)
plot(k8_glrm_regularized)

# Split data into train & validation
split <- h2o.splitFrame(my_basket.h2o, ratios = 0.75, seed = 123)
train <- split[[1]]
valid <- split[[2]]

# Create hyperparameter search grid
params <- expand.grid(
  regularization_x = c("None", "NonNegative", "L1"),
  regularization_y = c("None", "NonNegative", "L1"),
  gamma_x = seq(0, 1, by = .25),
  gamma_y = seq(0, 1, by = .25),
  error = 0,
  stringsAsFactors = FALSE
  )

# Perform grid search
for(i in seq_len(nrow(params))) {
  
  # Create model
  glrm_model <- h2o.glrm(
    training_frame = train,
    k = 8, 
    loss = "Quadratic",
    regularization_x = params$regularization_x[i], 
    regularization_y = params$regularization_y[i],
    gamma_x = params$gamma_x[i],
    gamma_y = params$gamma_y[i],
    transform = "STANDARDIZE", 
    max_runtime_secs = 1000,
    seed = 123
  )
  
  # Predict on validation set and extract error
  validate <- h2o.performance(glrm_model, valid)
  params$error[i] <- validate@metrics$numerr
}

# Look at the top 10 models with the lowest error rate
params %>%
  arrange(error) %>%
  head(10)
# Apply final model with optimal hyperparamters
final_glrm_model <- h2o.glrm(
  training_frame = my_basket.h2o,
  k = 8, 
  loss = "Quadratic",
  regularization_x = "L1", 
  regularization_y = "NonNegative",
  gamma_x = 1,
  gamma_y = 0.25,
  transform = "STANDARDIZE", 
  max_iterations = 2000,
  seed = 123
)

# New observations to score
new_observations <- as.h2o(sample_n(my_basket, 2))

# Basic scoring
predict(final_glrm_model, new_observations) %>% round(0)

[2 rows x 42 columns] 
h2o.shutdown(prompt = FALSE)
LS0tCnRpdGxlOiAiQ2hhcHRlciAxODogR2VuZXJhbGl6ZWQgTG93IFJhbmsgTW9kZWxzIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgpfX05vdGVfXzogU29tZSByZXN1bHRzIG1heSBkaWZmZXIgZnJvbSB0aGUgaGFyZCBjb3B5IGJvb2sgZHVlIHRvIHRoZSBjaGFuZ2luZyBvZgpzYW1wbGluZyBwcm9jZWR1cmVzIGludHJvZHVjZWQgaW4gUiAzLjYuMC4gU2VlIGh0dHA6Ly9iaXQubHkvMzVEMVNXNyBmb3IgbW9yZQpkZXRhaWxzLiBBY2Nlc3MgYW5kIHJ1biB0aGUgc291cmNlIGNvZGUgZm9yIHRoaXMgbm90ZWJvb2sgW2hlcmVdKGh0dHBzOi8vcnN0dWRpby5jbG91ZC9wcm9qZWN0LzgwMTE4NSkuCgpIaWRkZW4gY2hhcHRlciByZXF1aXJlbWVudHMgdXNlZCBpbiB0aGUgYm9vayB0byBzZXQgdGhlIHBsb3R0aW5nIHRoZW1lIGFuZCBsb2FkCnBhY2thZ2VzIHVzZWQgaW4gaGlkZGVuIGNvZGUgY2h1bmtzOgoKYGBge3Igc2V0dXB9CmtuaXRyOjpvcHRzX2NodW5rJHNldCgKICBtZXNzYWdlID0gRkFMU0UsIAogIHdhcm5pbmcgPSBGQUxTRSwgCiAgY2FjaGUgPSBGQUxTRQopCgojIFNldCB0aGUgZ3JhcGhpY2FsIHRoZW1lCmdncGxvdDI6OnRoZW1lX3NldChnZ3Bsb3QyOjp0aGVtZV9saWdodCgpKQpgYGAKCiMjIFByZXJlcXVpc2l0ZXMKClRoaXMgY2hhcHRlciBsZXZlcmFnZXMgdGhlIGZvbGxvd2luZyBwYWNrYWdlczoKCmBgYHtyIGdscm0tcGtnLXJlcX0KIyBIZWxwZXIgcGFja2FnZXMKbGlicmFyeShkcGx5cikgICAgIyBmb3IgZGF0YSBtYW5pcHVsYXRpb24KbGlicmFyeShnZ3Bsb3QyKSAgIyBmb3IgZGF0YSB2aXN1YWxpemF0aW9uCmxpYnJhcnkodGlkeXIpICAgICMgZm9yIGRhdGEgcmVzaGFwaW5nCgojIE1vZGVsaW5nIHBhY2thZ2VzCmxpYnJhcnkoaDJvKSAgIyBmb3IgZml0dGluZyBHTFJNcwpgYGAKClRvIGlsbHVzdHJhdGUgR0xSTSBjb25jZXB0cywgd2UnbGwgY29udGludWUgdXNpbmcgdGhlIGBteV9iYXNrZXRgIGRhdGEgc2V0IGNyZWF0ZWQgaW4gdGhlIHByZXZpb3VzIGNoYXB0ZXI6CgpgYGB7ciBnbHJtLWRhdGEtcmVxfQp1cmwgPC0gImh0dHBzOi8va29hbGF2ZXJzZS5naXRodWIuaW8vaG9tbHIvZGF0YS9teV9iYXNrZXQuY3N2IgpteV9iYXNrZXQgPC0gcmVhZHI6OnJlYWRfY3N2KHVybCkKYGBgCgoKIyMgVGhlIGlkZWEKCmBgYHtyIG10Y2Fycy10YWJsZX0KaGVhZChtdGNhcnMpCmBgYAoKRmlndXJlIDE4LjE6CgpgYGB7ciBnbHJtLWV4YW1wbGUsIGZpZy5jYXA9IkV4YW1wbGUgR0xSTSB3aGVyZSB3ZSByZWR1Y2UgdGhlIG10Y2FycyBkYXRhIHNldCBkb3duIHRvIGEgcmFuayBvZiAzLiIsIG91dC5oZWlnaHQ9IjEwMCUiLCBvdXQud2lkdGg9IjEwMCUifQprbml0cjo6aW5jbHVkZV9ncmFwaGljcygiaW1hZ2VzL2dscm0tZXhhbXBsZS5wbmciKQpgYGAKCiMjIEZpbmRpbmcgdGhlIGxvd2VyIHJhbmtzCgojIyMgTG9zcyBmdW5jdGlvbnMKCkZpZ3VyZSAxOC4yOgoKYGBge3IgcXVhZHJhdGljLXZzLWh1YmVyLCBmaWcuY2FwPSJIdWJlciBsb3NzIChncmVlbikgY29tcGFyZWQgdG8gcXVhZHJhdGljIGxvc3MgKGJsdWUpLiAgVGhlICR4JC1heGlzIHJlcHJlc2VudHMgYSBwYXJ0aWN1bGFyIHZhbHVlIGF0ICRBX3tpLGp9JCBhbmQgdGhlICR5JC1heGlzIHJlcHJlc2VudHMgdGhlIHByZWRpY3RlZCB2YWx1ZSBwcm9kdWNlZCBieSAkWF9pWV9qJC4gTm90ZSBob3cgdGhlIEh1YmVyIGxvc3MgcHJvZHVjZXMgYSBsaW5lYXIgbG9zcyB3aGlsZSB0aGUgcXVhZHJhdGljIGxvc3MgcHJvZHVjZXMgbXVjaCBsYXJnZXIgbG9zcyB2YWx1ZXMgYXMgdGhlIHJlc2lkdWFsIHZhbHVlIGluY3JlYXNlcy4ifQprbml0cjo6aW5jbHVkZV9ncmFwaGljcygiaW1hZ2VzL3F1YWRyYXRpYy1odWJlci1sb3NzLnBuZyIpCmBgYAoKIyMgRml0dGluZyBHTFJNcyBpbiBSCgpgYGB7ciBnbHJtLWgyby1pbml0fQpoMm8ubm9fcHJvZ3Jlc3MoKSAgIyB0dXJuIG9mZiBwcm9ncmVzcyBiYXJzCmgyby5pbml0KG1heF9tZW1fc2l6ZSA9ICI1ZyIpICAjIGNvbm5lY3QgdG8gSDJPIGluc3RhbmNlCmBgYAoKIyMjIEJhc2ljIEdMUk0gbW9kZWwKCmBgYHtyIGdscm0tbW9kZWwtMX0KIyBjb252ZXJ0IGRhdGEgdG8gaDJvIG9iamVjdApteV9iYXNrZXQuaDJvIDwtIGFzLmgybyhteV9iYXNrZXQpCgojIHJ1biBiYXNpYyBHTFJNCmJhc2ljX2dscm0gPC0gaDJvLmdscm0oCiAgdHJhaW5pbmdfZnJhbWUgPSBteV9iYXNrZXQuaDJvLAogIGsgPSAyMCwgCiAgbG9zcyA9ICJRdWFkcmF0aWMiLAogIHJlZ3VsYXJpemF0aW9uX3ggPSAiTm9uZSIsIAogIHJlZ3VsYXJpemF0aW9uX3kgPSAiTm9uZSIsIAogIHRyYW5zZm9ybSA9ICJTVEFOREFSRElaRSIsIAogIG1heF9pdGVyYXRpb25zID0gMjAwMCwKICBzZWVkID0gMTIzCikKYGBgCgpgYGB7ciBnbHJtLW1vZDEtcmVzdWx0c30KIyBnZXQgdG9wIGxldmVsIHN1bW1hcnkgaW5mb3JtYXRpb24gb24gb3VyIG1vZGVsCnN1bW1hcnkoYmFzaWNfZ2xybSkKCnBsb3QoYmFzaWNfZ2xybSkKYGBgCgpgYGB7ciBhcmNoZXR5cGUtdmFyaWFuY2UtZXhwbGFpbmVkfQojIGFtb3VudCBvZiB2YXJpYW5jZSBleHBsYWluZWQgYnkgZWFjaCBhcmNoZXR5cGUgKGFrYSAicGMiKQpiYXNpY19nbHJtQG1vZGVsJGltcG9ydGFuY2UKYGBgCgpgYGB7ciBnbHJtLW1vZDEtcGxvdC12YXJpYW5jZS1leHBsYWluZWQsIGZpZy5oZWlnaHQ9NSwgZmlnLndpZHRoPTksIGZpZy5jYXA9IlZhcmlhbmNlIGV4cGxhaW5lZCBieSB0aGUgZmlyc3QgMjAgYXJjaGV0eXBlcyBpbiBvdXIgR0xSTSBtb2RlbC4ifQpkYXRhLmZyYW1lKAogICAgUEMgID0gYmFzaWNfZ2xybUBtb2RlbCRpbXBvcnRhbmNlICU+JSBzZXFfYWxvbmcoKSwKICAgIFBWRSA9IGJhc2ljX2dscm1AbW9kZWwkaW1wb3J0YW5jZSAlPiUgLlsyLF0gJT4lIHVubGlzdCgpLAogICAgQ1ZFID0gYmFzaWNfZ2xybUBtb2RlbCRpbXBvcnRhbmNlICU+JSAuWzMsXSAlPiUgdW5saXN0KCkKKSAlPiUKICAgIGdhdGhlcihtZXRyaWMsIHZhcmlhbmNlX2V4cGxhaW5lZCwgLVBDKSAlPiUKICAgIGdncGxvdChhZXMoUEMsIHZhcmlhbmNlX2V4cGxhaW5lZCkpICsKICAgIGdlb21fcG9pbnQoKSArCiAgICBmYWNldF93cmFwKH4gbWV0cmljLCBuY29sID0gMSwgc2NhbGVzID0gImZyZWUiKQpgYGAKCmBgYHtyIGdscm0tZ2V0LWFyY2hldHlwZXN9CnQoYmFzaWNfZ2xybUBtb2RlbCRhcmNoZXR5cGVzKVsxOjUsIDE6NV0KYGBgCgpgYGB7ciBnbHJtLXBsb3QtYXJjaGV0eXBlcywgZmlnLndpZHRoPTExLCBmaWcuaGVpZ2h0PTUsIGZpZy5jYXA9IkZlYXR1cmUgY29udHJpYnV0aW9uIGZvciBhcmNoZXR5cGUgMSBhbmQgMi4ifQpwMSA8LSB0KGJhc2ljX2dscm1AbW9kZWwkYXJjaGV0eXBlcykgJT4lIAogIGFzLmRhdGEuZnJhbWUoKSAlPiUgCiAgbXV0YXRlKGZlYXR1cmUgPSByb3cubmFtZXMoLikpICU+JQogIGdncGxvdChhZXMoQXJjaDEsIHJlb3JkZXIoZmVhdHVyZSwgQXJjaDEpKSkgKwogIGdlb21fcG9pbnQoKQoKcDIgPC0gdChiYXNpY19nbHJtQG1vZGVsJGFyY2hldHlwZXMpICU+JSAKICBhcy5kYXRhLmZyYW1lKCkgJT4lIAogIG11dGF0ZShmZWF0dXJlID0gcm93Lm5hbWVzKC4pKSAlPiUKICBnZ3Bsb3QoYWVzKEFyY2gxLCBBcmNoMiwgbGFiZWwgPSBmZWF0dXJlKSkgKwogIGdlb21fdGV4dCgpCgpncmlkRXh0cmE6OmdyaWQuYXJyYW5nZShwMSwgcDIsIG5yb3cgPSAxKQpgYGAKCmBgYHtyIGdscm0tcmVjb25zdHJ1Y3R9CiMgUmUtcnVuIG1vZGVsIHdpdGggayA9IDgKazhfZ2xybSA8LSBoMm8uZ2xybSgKICB0cmFpbmluZ19mcmFtZSA9IG15X2Jhc2tldC5oMm8sCiAgayA9IDgsIAogIGxvc3MgPSAiUXVhZHJhdGljIiwKICByZWd1bGFyaXphdGlvbl94ID0gIk5vbmUiLCAKICByZWd1bGFyaXphdGlvbl95ID0gIk5vbmUiLCAKICB0cmFuc2Zvcm0gPSAiU1RBTkRBUkRJWkUiLCAKICBtYXhfaXRlcmF0aW9ucyA9IDIwMDAsCiAgc2VlZCA9IDEyMwopCgojIFJlY29uc3RydWN0IHRvIHNlZSBob3cgd2VsbCB0aGUgbW9kZWwgZGlkCm15X3JlY29uc3RydWN0aW9uIDwtIGgyby5yZWNvbnN0cnVjdChrOF9nbHJtLCBteV9iYXNrZXQuaDJvLCByZXZlcnNlX3RyYW5zZm9ybSA9IFRSVUUpCgojIFJhdyBwcmVkaWN0ZWQgdmFsdWVzCm15X3JlY29uc3RydWN0aW9uWzE6NSwgMTo1XQoKIyBSb3VuZCB2YWx1ZXMgdG8gd2hvbGUgaW50ZWdlcnMKbXlfcmVjb25zdHJ1Y3Rpb25bMTo1LCAxOjVdICU+JSByb3VuZCgwKQpgYGAKCiMjIyBUdW5pbmcgdG8gb3B0aW1pemUgZm9yIHVuc2VlbiBkYXRhCgpgYGB7cn0KIyBVc2Ugbm9uLW5lZ2F0aXZlIHJlZ3VsYXJpemF0aW9uCms4X2dscm1fcmVndWxhcml6ZWQgPC0gaDJvLmdscm0oCiAgdHJhaW5pbmdfZnJhbWUgPSBteV9iYXNrZXQuaDJvLAogIGsgPSA4LCAKICBsb3NzID0gIlF1YWRyYXRpYyIsCiAgcmVndWxhcml6YXRpb25feCA9ICJOb25OZWdhdGl2ZSIsIAogIHJlZ3VsYXJpemF0aW9uX3kgPSAiTm9uTmVnYXRpdmUiLAogIGdhbW1hX3ggPSAwLjUsCiAgZ2FtbWFfeSA9IDAuNSwKICB0cmFuc2Zvcm0gPSAiU1RBTkRBUkRJWkUiLCAKICBtYXhfaXRlcmF0aW9ucyA9IDIwMDAsCiAgc2VlZCA9IDEyMwopCgojIFNob3cgcHJlZGljdGVkIHZhbHVlcwpwcmVkaWN0KGs4X2dscm1fcmVndWxhcml6ZWQsIG15X2Jhc2tldC5oMm8pWzE6NSwgMTo1XQoKIyBDb21wYXJlIHJlZ3VsYXJpemVkIHZlcnN1cyBub24tcmVndWxhcml6ZWQgbG9zcwpwYXIobWZyb3cgPSBjKDEsIDIpKQpwbG90KGs4X2dscm0pCnBsb3QoazhfZ2xybV9yZWd1bGFyaXplZCkKYGBgCgpgYGB7cn0KIyBTcGxpdCBkYXRhIGludG8gdHJhaW4gJiB2YWxpZGF0aW9uCnNwbGl0IDwtIGgyby5zcGxpdEZyYW1lKG15X2Jhc2tldC5oMm8sIHJhdGlvcyA9IDAuNzUsIHNlZWQgPSAxMjMpCnRyYWluIDwtIHNwbGl0W1sxXV0KdmFsaWQgPC0gc3BsaXRbWzJdXQoKIyBDcmVhdGUgaHlwZXJwYXJhbWV0ZXIgc2VhcmNoIGdyaWQKcGFyYW1zIDwtIGV4cGFuZC5ncmlkKAogIHJlZ3VsYXJpemF0aW9uX3ggPSBjKCJOb25lIiwgIk5vbk5lZ2F0aXZlIiwgIkwxIiksCiAgcmVndWxhcml6YXRpb25feSA9IGMoIk5vbmUiLCAiTm9uTmVnYXRpdmUiLCAiTDEiKSwKICBnYW1tYV94ID0gc2VxKDAsIDEsIGJ5ID0gLjI1KSwKICBnYW1tYV95ID0gc2VxKDAsIDEsIGJ5ID0gLjI1KSwKICBlcnJvciA9IDAsCiAgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFCiAgKQoKIyBQZXJmb3JtIGdyaWQgc2VhcmNoCmZvcihpIGluIHNlcV9sZW4obnJvdyhwYXJhbXMpKSkgewogIAogICMgQ3JlYXRlIG1vZGVsCiAgZ2xybV9tb2RlbCA8LSBoMm8uZ2xybSgKICAgIHRyYWluaW5nX2ZyYW1lID0gdHJhaW4sCiAgICBrID0gOCwgCiAgICBsb3NzID0gIlF1YWRyYXRpYyIsCiAgICByZWd1bGFyaXphdGlvbl94ID0gcGFyYW1zJHJlZ3VsYXJpemF0aW9uX3hbaV0sIAogICAgcmVndWxhcml6YXRpb25feSA9IHBhcmFtcyRyZWd1bGFyaXphdGlvbl95W2ldLAogICAgZ2FtbWFfeCA9IHBhcmFtcyRnYW1tYV94W2ldLAogICAgZ2FtbWFfeSA9IHBhcmFtcyRnYW1tYV95W2ldLAogICAgdHJhbnNmb3JtID0gIlNUQU5EQVJESVpFIiwgCiAgICBtYXhfcnVudGltZV9zZWNzID0gMTAwMCwKICAgIHNlZWQgPSAxMjMKICApCiAgCiAgIyBQcmVkaWN0IG9uIHZhbGlkYXRpb24gc2V0IGFuZCBleHRyYWN0IGVycm9yCiAgdmFsaWRhdGUgPC0gaDJvLnBlcmZvcm1hbmNlKGdscm1fbW9kZWwsIHZhbGlkKQogIHBhcmFtcyRlcnJvcltpXSA8LSB2YWxpZGF0ZUBtZXRyaWNzJG51bWVycgp9CgojIExvb2sgYXQgdGhlIHRvcCAxMCBtb2RlbHMgd2l0aCB0aGUgbG93ZXN0IGVycm9yIHJhdGUKcGFyYW1zICU+JQogIGFycmFuZ2UoZXJyb3IpICU+JQogIGhlYWQoMTApCmBgYAoKYGBge3IgZmluYWwtbW9kZWwtc2NvcmluZ30KIyBBcHBseSBmaW5hbCBtb2RlbCB3aXRoIG9wdGltYWwgaHlwZXJwYXJhbXRlcnMKZmluYWxfZ2xybV9tb2RlbCA8LSBoMm8uZ2xybSgKICB0cmFpbmluZ19mcmFtZSA9IG15X2Jhc2tldC5oMm8sCiAgayA9IDgsIAogIGxvc3MgPSAiUXVhZHJhdGljIiwKICByZWd1bGFyaXphdGlvbl94ID0gIkwxIiwgCiAgcmVndWxhcml6YXRpb25feSA9ICJOb25OZWdhdGl2ZSIsCiAgZ2FtbWFfeCA9IDEsCiAgZ2FtbWFfeSA9IDAuMjUsCiAgdHJhbnNmb3JtID0gIlNUQU5EQVJESVpFIiwgCiAgbWF4X2l0ZXJhdGlvbnMgPSAyMDAwLAogIHNlZWQgPSAxMjMKKQoKIyBOZXcgb2JzZXJ2YXRpb25zIHRvIHNjb3JlCm5ld19vYnNlcnZhdGlvbnMgPC0gYXMuaDJvKHNhbXBsZV9uKG15X2Jhc2tldCwgMikpCgojIEJhc2ljIHNjb3JpbmcKcHJlZGljdChmaW5hbF9nbHJtX21vZGVsLCBuZXdfb2JzZXJ2YXRpb25zKSAlPiUgcm91bmQoMCkKYGBgCgpgYGB7ciBnbHJtLXNodXRkb3duLWgyb30KaDJvLnNodXRkb3duKHByb21wdCA9IEZBTFNFKQpgYGA=