This project is designed to test your current knowledge on applying word embeddings to the Amazon Fine Foods reviews dataset available through Stanford. This dataset contains 568,454 reviews on 74,258 products.

Your goal is to develop a word embedding model to accurately predict how helpful a review will be. I supply code to help you get the data imported and prepped so that you can focus on the modeling aspect.

Good luck!

Requirements

library(keras)     # provides deep learning procedures
library(tidyverse) # provides basic data wrangling and visualization
library(glue)      # provides efficient print statements
library(testthat)  # provides unit testing

Data importing

The finefoods.txt.gz file has already been downloaded and unzipped for you. All reviews are contained in a single .txt file.

# get path to data
if (stringr::str_detect(here::here(), "conf-2020-user")) {
  amazon_reviews <- "/home/conf-2020-user/data/amazon-food/finefoods.txt"
} else {
  amazon_reviews <- here::here("materials", "data", "amazon-food", "finefoods.txt")
}

reviews <- read_lines(amazon_reviews)

Each review consists of 8 items and each item is on its own line. The following shows all information collected for the first review.

head(reviews, 8)

Verify we properly imported

Based on the data’s website, we should have the following:

  • Number of reviews: 568,454
  • Number of products: 74,258
  • Number of users: 256,059
review_text <- reviews[str_detect(reviews, "review/text:")]
products <- reviews[str_detect(reviews, "product/productId:")]
users <- reviews[str_detect(reviews, "review/userId:")]

n_reviews <- length(review_text)
n_products <- n_distinct(products)
n_users <- n_distinct(users)

# Verify our imported data aligns with data codebook
expect_equal(n_reviews, 568454)
expect_equal(n_products, 74258)
expect_equal(n_users, 256059)

Extracting key parts of the data

There are two main parts of these reviews that we need for our modeling purpose:

  1. The review text
  2. The fraction of users who found the review helpful

Getting our text

Let’s extract the text

text <- review_text %>%
  str_replace("review/text:", "") %>%
  iconv(to = "UTF-8") %>%
  str_trim()

expect_equal(length(text), n_reviews)

text[1]

Getting our labels

Now let’s extract our helpfulness information. This represents the fraction of users who found the review helpful for a given product.

helpfulness_info <- reviews[str_detect(reviews, "review/helpfulness:")] %>%
  str_extract("\\d.*")

expect_equal(length(helpfulness_info), n_reviews)

head(helpfulness_info)

Let’s separate this information into the number of reviews (denominator) and the number of user who found the review helpful (numerator).

num_reviews <- str_replace(helpfulness_info, "^.*\\/", "") %>% as.integer()
helpfulness <- str_replace(helpfulness_info, "\\/.*$", "") %>% as.integer()

And we’re only going to care about those products with 10+ reviews to try minimize some of the noise.

num_index <- num_reviews >= 10
num_reviews <- num_reviews[num_index]
helpfulness <- helpfulness[num_index]
text <- text[num_index]

# verify that the number of observations in each vector is equal
expect_equal(
  map_int(list(num_reviews, helpfulness, text), length) %>% n_distinct(),
  1
)

glue("There are {sum(num_index)} observations with 10 or more reviews.")

Our labels are going to be the fraction provided by helpfulness converted to a percentage.

labels <- helpfulness / num_reviews

expect_equal(length(labels), length(text))

range(labels)

We can look at a review that is considered very helpful…

first_pos <- first(which(labels == 1))
text[first_pos]

versus a review that is considered very unhelpful.

first_neg <- first(which(labels == 0))
text[first_neg]

Let’s get a quick assessment of word usage across the reviews:

text_df <- text %>%
  tibble(.name_repair = ~ "text") %>%
  mutate(text_length = str_trim(text) %>% str_count("\\w+"))

unique_words <- text_df %>%
  tidytext::unnest_tokens(word, text) %>%
  pull(word) %>%
  n_distinct()

avg_review_length <- median(text_df$text_length, na.rm = TRUE)
  
ggplot(text_df, aes(text_length)) +
  geom_histogram(bins = 100, fill = "grey70", color = "grey40") +
  geom_vline(xintercept = avg_review_length, color = "red", lty = "dashed") +
  scale_x_log10() +
  ggtitle(glue("Median review length is {avg_review_length}"),
          subtitle = glue("Total number of unique words is {unique_words}"))

Explore Glove Embeddings

We can explore word embeddings that give us some context of the review language.

# helper functions we'll use to explore word embeddings
source("helper_functions.R")

# clean up text and compute word embeddings
clean_text <- tolower(text) %>%
  str_replace_all(pattern = "[[:punct:] ]+", replacement = " ") %>%
  str_trim()

word_embeddings <- get_embeddings(clean_text)

Explore your own words!

# find words with similar embeddings
get_similar_words("oil", word_embeddings)

Prepare data

Our labels are already a tensor (vector) so we don’t need to do any additional prep.

str(labels)

Preprocessing hyperparameters

However, we need to preprocess our text. First, lets decide on two key parameters to use when preprocessing our text:

  1. number of most frequent words used (start with 20000)
  2. the maximum length of our processed text (start with 200)

These are two hyperparameters you can come back to and change as hyperparameters.

top_n_words <- ______
max_len <- ______

Preprocessing Feature text

Next, you need to create and apply a tokenizer to the text.

tokenizer <- text_tokenizer(num_words = top_n_words) %>% 
  fit_text_tokenizer(text)

names(tokenizer)

Now, convert your text to a numerically encoded sequence.

sequences <- texts_to_sequences(tokenizer, text)
# The vectorized first instance:
sequences[[1]]

Run this code chunk to see how your text has been converted:

cat(crayon::blue("Original text:\n"))
text[[1]]

cat(crayon::blue("\nRevised text:\n"))
paste(unlist(tokenizer$index_word)[sequences[[1]]] , collapse = " ")

Last, we want to make sure our sequences (aka each processed review) is of equal length.

features <- pad_sequences(sequences, maxlen = max_len)

expect_equal(ncol(features), max_len)

Make sure that the number of observations in your features and labels are equal:

expect_equal(nrow(features), length(labels))

Model training

Before we train our model, let’s go ahead and randomize our review data so that our training and validation data properly represent a mixture of products and users.

set.seed(123)
index <- sample(1:nrow(features))
split_point <- floor(length(index) * .3)
train_index <- index[1:split_point]
valid_index <- index[(split_point + 1):length(index)]

expect_equal(length(train_index) + length(valid_index), length(index))

x_train <- features[train_index, ]
y_train <- labels[train_index]

x_valid <- features[valid_index, ]
y_valid <- labels[valid_index]

Ok, so before we train our model, let’s get an understanding of a baseline loss score that we want to beat. The easiest baseline is to just predict the average of the training label for future observations.

avg <- mean(y_train)
baseline_mse <- mean((y_valid - avg)^2)

cat("Simply predicting the average helpfulness score of", round(avg, 2),
    "for every review would give us a loss score of", round(baseline_mse, 3))

Ok, time to build your model architecture and compile it. Fill in the modeling blanks and consider the following:

  1. Your word embedding input_dim was already established with top_n_words
    • Ref: line 226
    • feel free to change this values and see how they impact performance
  2. Your word embedding input_length was already established with max_len on
    • Ref: line 227
    • feel free to change this values and see how they impact performance
  3. Try out different output_dim values for the word embeddings
    • typical values: powers of 2 –> 16, 32, 64, 128, 256
  4. Feel free to add additional hidden layers and dropout layers to the densely connected classifier.
model <- keras_model_sequential() %>%
  layer_embedding(input_dim = _____, 
                  output_dim = _____,
                  input_length = _____) %>% 
  layer_flatten() %>%
  layer_dense(units = 1)

model %>% compile(
  optimizer = _____,
  loss = "mse",
  metrics = _____
)

summary(model)

Let’s train our model:

history <- model %>% fit(
  x_train, y_train,
  epochs = _____,
  batch_size = _____,
  validation_data = list(x_valid, y_valid),
  callbacks = list(
    callback_reduce_lr_on_plateau(patience = _____),
    callback_early_stopping(patience = _____, restore_best_weights = TRUE)
    )
)

Let’s compare the optimal loss score versus the baseline loss score.

opt_mse <- min(history$metrics$val_loss)
glue("Baseline loss score: {round(baseline_mse, 3)}")
glue("Model loss score: {round(opt_mse, 3)}")
LS0tCnRpdGxlOiAiTkxQOiBUcmFuc2ZlciBsZWFybmluZyBmb3IgQW1hem9uIHJldmlldyB3b3JkIGVtYmVkZGluZ3MiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgdG9jOiB5ZXMKICAgIHRvY19mbG9hdDogdHJ1ZQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UpCmBgYAoKVGhpcyBwcm9qZWN0IGlzIGRlc2lnbmVkIHRvIHRlc3QgeW91ciBjdXJyZW50IGtub3dsZWRnZSBvbiBhcHBseWluZyB3b3JkCmVtYmVkZGluZ3MgdG8gdGhlIFtBbWF6b24gRmluZSBGb29kcyByZXZpZXdzXShodHRwczovL3NuYXAuc3RhbmZvcmQuZWR1L2RhdGEvd2ViLUZpbmVGb29kcy5odG1sKSAKZGF0YXNldCBhdmFpbGFibGUgdGhyb3VnaCBTdGFuZm9yZC4gVGhpcyBkYXRhc2V0IGNvbnRhaW5zIDU2OCw0NTQgcmV2aWV3cyBvbgo3NCwyNTggcHJvZHVjdHMuCgpZb3VyIGdvYWwgaXMgdG8gZGV2ZWxvcCBhIHdvcmQgZW1iZWRkaW5nIG1vZGVsIHRvIGFjY3VyYXRlbHkgcHJlZGljdCBob3cgCl9faGVscGZ1bF9fIGEgcmV2aWV3IHdpbGwgYmUuIEkgc3VwcGx5IGNvZGUgdG8gaGVscCB5b3UgZ2V0IHRoZSBkYXRhCmltcG9ydGVkIGFuZCBwcmVwcGVkIHNvIHRoYXQgeW91IGNhbiBmb2N1cyBvbiB0aGUgbW9kZWxpbmcgYXNwZWN0LgoKX19fR29vZCBsdWNrIV9fXwoKIyBSZXF1aXJlbWVudHMKCmBgYHtyfQpsaWJyYXJ5KGtlcmFzKSAgICAgIyBwcm92aWRlcyBkZWVwIGxlYXJuaW5nIHByb2NlZHVyZXMKbGlicmFyeSh0aWR5dmVyc2UpICMgcHJvdmlkZXMgYmFzaWMgZGF0YSB3cmFuZ2xpbmcgYW5kIHZpc3VhbGl6YXRpb24KbGlicmFyeShnbHVlKSAgICAgICMgcHJvdmlkZXMgZWZmaWNpZW50IHByaW50IHN0YXRlbWVudHMKbGlicmFyeSh0ZXN0dGhhdCkgICMgcHJvdmlkZXMgdW5pdCB0ZXN0aW5nCmBgYAoKIyBEYXRhIGltcG9ydGluZwoKVGhlIFtmaW5lZm9vZHMudHh0Lmd6XShodHRwczovL3NuYXAuc3RhbmZvcmQuZWR1L2RhdGEvZmluZWZvb2RzLnR4dC5neikgZmlsZSBoYXMKYWxyZWFkeSBiZWVuIGRvd25sb2FkZWQgYW5kIHVuemlwcGVkIGZvciB5b3UuIEFsbCByZXZpZXdzIGFyZSBjb250YWluZWQgaW4gYQpzaW5nbGUgLnR4dCBmaWxlLgoKYGBge3J9CiMgZ2V0IHBhdGggdG8gZGF0YQppZiAoc3RyaW5ncjo6c3RyX2RldGVjdChoZXJlOjpoZXJlKCksICJjb25mLTIwMjAtdXNlciIpKSB7CiAgYW1hem9uX3Jldmlld3MgPC0gIi9ob21lL2NvbmYtMjAyMC11c2VyL2RhdGEvYW1hem9uLWZvb2QvZmluZWZvb2RzLnR4dCIKfSBlbHNlIHsKICBhbWF6b25fcmV2aWV3cyA8LSBoZXJlOjpoZXJlKCJtYXRlcmlhbHMiLCAiZGF0YSIsICJhbWF6b24tZm9vZCIsICJmaW5lZm9vZHMudHh0IikKfQoKcmV2aWV3cyA8LSByZWFkX2xpbmVzKGFtYXpvbl9yZXZpZXdzKQpgYGAKCkVhY2ggcmV2aWV3IGNvbnNpc3RzIG9mIDggaXRlbXMgYW5kIGVhY2ggaXRlbSBpcyBvbiBpdHMgb3duIGxpbmUuIFRoZSBmb2xsb3dpbmcKc2hvd3MgYWxsIGluZm9ybWF0aW9uIGNvbGxlY3RlZCBmb3IgdGhlIGZpcnN0IHJldmlldy4KCmBgYHtyfQpoZWFkKHJldmlld3MsIDgpCmBgYAoKIyBWZXJpZnkgd2UgcHJvcGVybHkgaW1wb3J0ZWQKCkJhc2VkIG9uIHRoZSBkYXRhJ3MgW3dlYnNpdGVdKGh0dHBzOi8vc25hcC5zdGFuZm9yZC5lZHUvZGF0YS93ZWItRmluZUZvb2RzLmh0bWwpLAp3ZSBzaG91bGQgaGF2ZSB0aGUgZm9sbG93aW5nOgoKLSBOdW1iZXIgb2YgcmV2aWV3czogNTY4LDQ1NAotIE51bWJlciBvZiBwcm9kdWN0czogNzQsMjU4Ci0gTnVtYmVyIG9mIHVzZXJzOiAyNTYsMDU5CgpgYGB7cn0KcmV2aWV3X3RleHQgPC0gcmV2aWV3c1tzdHJfZGV0ZWN0KHJldmlld3MsICJyZXZpZXcvdGV4dDoiKV0KcHJvZHVjdHMgPC0gcmV2aWV3c1tzdHJfZGV0ZWN0KHJldmlld3MsICJwcm9kdWN0L3Byb2R1Y3RJZDoiKV0KdXNlcnMgPC0gcmV2aWV3c1tzdHJfZGV0ZWN0KHJldmlld3MsICJyZXZpZXcvdXNlcklkOiIpXQoKbl9yZXZpZXdzIDwtIGxlbmd0aChyZXZpZXdfdGV4dCkKbl9wcm9kdWN0cyA8LSBuX2Rpc3RpbmN0KHByb2R1Y3RzKQpuX3VzZXJzIDwtIG5fZGlzdGluY3QodXNlcnMpCgojIFZlcmlmeSBvdXIgaW1wb3J0ZWQgZGF0YSBhbGlnbnMgd2l0aCBkYXRhIGNvZGVib29rCmV4cGVjdF9lcXVhbChuX3Jldmlld3MsIDU2ODQ1NCkKZXhwZWN0X2VxdWFsKG5fcHJvZHVjdHMsIDc0MjU4KQpleHBlY3RfZXF1YWwobl91c2VycywgMjU2MDU5KQpgYGAKCgojIEV4dHJhY3Rpbmcga2V5IHBhcnRzIG9mIHRoZSBkYXRhCgpUaGVyZSBhcmUgdHdvIG1haW4gcGFydHMgb2YgdGhlc2UgcmV2aWV3cyB0aGF0IHdlIG5lZWQgZm9yIG91ciBtb2RlbGluZyBwdXJwb3NlOgoKMS4gVGhlIHJldmlldyB0ZXh0CjIuIFRoZSBmcmFjdGlvbiBvZiB1c2VycyB3aG8gZm91bmQgdGhlIHJldmlldyBoZWxwZnVsCgojIyBHZXR0aW5nIG91ciB0ZXh0CgpMZXQncyBleHRyYWN0IHRoZSB0ZXh0CgpgYGB7cn0KdGV4dCA8LSByZXZpZXdfdGV4dCAlPiUKICBzdHJfcmVwbGFjZSgicmV2aWV3L3RleHQ6IiwgIiIpICU+JQogIGljb252KHRvID0gIlVURi04IikgJT4lCiAgc3RyX3RyaW0oKQoKZXhwZWN0X2VxdWFsKGxlbmd0aCh0ZXh0KSwgbl9yZXZpZXdzKQoKdGV4dFsxXQpgYGAKCiMjIEdldHRpbmcgb3VyIGxhYmVscwoKTm93IGxldCdzIGV4dHJhY3Qgb3VyIGhlbHBmdWxuZXNzIGluZm9ybWF0aW9uLiBUaGlzIHJlcHJlc2VudHMgdGhlIGZyYWN0aW9uIG9mCnVzZXJzIHdobyBmb3VuZCB0aGUgcmV2aWV3IGhlbHBmdWwgZm9yIGEgZ2l2ZW4gcHJvZHVjdC4KCmBgYHtyfQpoZWxwZnVsbmVzc19pbmZvIDwtIHJldmlld3Nbc3RyX2RldGVjdChyZXZpZXdzLCAicmV2aWV3L2hlbHBmdWxuZXNzOiIpXSAlPiUKICBzdHJfZXh0cmFjdCgiXFxkLioiKQoKZXhwZWN0X2VxdWFsKGxlbmd0aChoZWxwZnVsbmVzc19pbmZvKSwgbl9yZXZpZXdzKQoKaGVhZChoZWxwZnVsbmVzc19pbmZvKQpgYGAKCkxldCdzIHNlcGFyYXRlIHRoaXMgaW5mb3JtYXRpb24gaW50byB0aGUgbnVtYmVyIG9mIHJldmlld3MgKGRlbm9taW5hdG9yKSBhbmQKdGhlIG51bWJlciBvZiB1c2VyIHdobyBmb3VuZCB0aGUgcmV2aWV3IGhlbHBmdWwgKG51bWVyYXRvcikuCgpgYGB7cn0KbnVtX3Jldmlld3MgPC0gc3RyX3JlcGxhY2UoaGVscGZ1bG5lc3NfaW5mbywgIl4uKlxcLyIsICIiKSAlPiUgYXMuaW50ZWdlcigpCmhlbHBmdWxuZXNzIDwtIHN0cl9yZXBsYWNlKGhlbHBmdWxuZXNzX2luZm8sICJcXC8uKiQiLCAiIikgJT4lIGFzLmludGVnZXIoKQpgYGAKCkFuZCB3ZSdyZSBvbmx5IGdvaW5nIHRvIGNhcmUgYWJvdXQgdGhvc2UgcHJvZHVjdHMgd2l0aCAxMCsgcmV2aWV3cyB0byB0cnkKbWluaW1pemUgc29tZSBvZiB0aGUgbm9pc2UuCgpgYGB7cn0KbnVtX2luZGV4IDwtIG51bV9yZXZpZXdzID49IDEwCm51bV9yZXZpZXdzIDwtIG51bV9yZXZpZXdzW251bV9pbmRleF0KaGVscGZ1bG5lc3MgPC0gaGVscGZ1bG5lc3NbbnVtX2luZGV4XQp0ZXh0IDwtIHRleHRbbnVtX2luZGV4XQoKIyB2ZXJpZnkgdGhhdCB0aGUgbnVtYmVyIG9mIG9ic2VydmF0aW9ucyBpbiBlYWNoIHZlY3RvciBpcyBlcXVhbApleHBlY3RfZXF1YWwoCiAgbWFwX2ludChsaXN0KG51bV9yZXZpZXdzLCBoZWxwZnVsbmVzcywgdGV4dCksIGxlbmd0aCkgJT4lIG5fZGlzdGluY3QoKSwKICAxCikKCmdsdWUoIlRoZXJlIGFyZSB7c3VtKG51bV9pbmRleCl9IG9ic2VydmF0aW9ucyB3aXRoIDEwIG9yIG1vcmUgcmV2aWV3cy4iKQpgYGAKCk91ciBsYWJlbHMgYXJlIGdvaW5nIHRvIGJlIHRoZSBmcmFjdGlvbiBwcm92aWRlZCBieSBoZWxwZnVsbmVzcyBjb252ZXJ0ZWQgdG8gYQpwZXJjZW50YWdlLgoKYGBge3J9CmxhYmVscyA8LSBoZWxwZnVsbmVzcyAvIG51bV9yZXZpZXdzCgpleHBlY3RfZXF1YWwobGVuZ3RoKGxhYmVscyksIGxlbmd0aCh0ZXh0KSkKCnJhbmdlKGxhYmVscykKYGBgCgpXZSBjYW4gbG9vayBhdCBhIHJldmlldyB0aGF0IGlzIGNvbnNpZGVyZWQgdmVyeSBoZWxwZnVsLi4uCgpgYGB7cn0KZmlyc3RfcG9zIDwtIGZpcnN0KHdoaWNoKGxhYmVscyA9PSAxKSkKdGV4dFtmaXJzdF9wb3NdCmBgYAoKdmVyc3VzIGEgcmV2aWV3IHRoYXQgaXMgY29uc2lkZXJlZCB2ZXJ5IHVuaGVscGZ1bC4KCmBgYHtyfQpmaXJzdF9uZWcgPC0gZmlyc3Qod2hpY2gobGFiZWxzID09IDApKQp0ZXh0W2ZpcnN0X25lZ10KYGBgCgpMZXQncyBnZXQgYSBxdWljayBhc3Nlc3NtZW50IG9mIHdvcmQgdXNhZ2UgYWNyb3NzIHRoZSByZXZpZXdzOgoKYGBge3J9CnRleHRfZGYgPC0gdGV4dCAlPiUKICB0aWJibGUoLm5hbWVfcmVwYWlyID0gfiAidGV4dCIpICU+JQogIG11dGF0ZSh0ZXh0X2xlbmd0aCA9IHN0cl90cmltKHRleHQpICU+JSBzdHJfY291bnQoIlxcdysiKSkKCnVuaXF1ZV93b3JkcyA8LSB0ZXh0X2RmICU+JQogIHRpZHl0ZXh0Ojp1bm5lc3RfdG9rZW5zKHdvcmQsIHRleHQpICU+JQogIHB1bGwod29yZCkgJT4lCiAgbl9kaXN0aW5jdCgpCgphdmdfcmV2aWV3X2xlbmd0aCA8LSBtZWRpYW4odGV4dF9kZiR0ZXh0X2xlbmd0aCwgbmEucm0gPSBUUlVFKQogIApnZ3Bsb3QodGV4dF9kZiwgYWVzKHRleHRfbGVuZ3RoKSkgKwogIGdlb21faGlzdG9ncmFtKGJpbnMgPSAxMDAsIGZpbGwgPSAiZ3JleTcwIiwgY29sb3IgPSAiZ3JleTQwIikgKwogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IGF2Z19yZXZpZXdfbGVuZ3RoLCBjb2xvciA9ICJyZWQiLCBsdHkgPSAiZGFzaGVkIikgKwogIHNjYWxlX3hfbG9nMTAoKSArCiAgZ2d0aXRsZShnbHVlKCJNZWRpYW4gcmV2aWV3IGxlbmd0aCBpcyB7YXZnX3Jldmlld19sZW5ndGh9IiksCiAgICAgICAgICBzdWJ0aXRsZSA9IGdsdWUoIlRvdGFsIG51bWJlciBvZiB1bmlxdWUgd29yZHMgaXMge3VuaXF1ZV93b3Jkc30iKSkKYGBgCgoKIyBFeHBsb3JlIEdsb3ZlIEVtYmVkZGluZ3MKCldlIGNhbiBleHBsb3JlIHdvcmQgZW1iZWRkaW5ncyB0aGF0IGdpdmUgdXMgc29tZSBjb250ZXh0IG9mIHRoZSByZXZpZXcgbGFuZ3VhZ2UuCgpgYGB7cn0KIyBoZWxwZXIgZnVuY3Rpb25zIHdlJ2xsIHVzZSB0byBleHBsb3JlIHdvcmQgZW1iZWRkaW5ncwpzb3VyY2UoImhlbHBlcl9mdW5jdGlvbnMuUiIpCgojIGNsZWFuIHVwIHRleHQgYW5kIGNvbXB1dGUgd29yZCBlbWJlZGRpbmdzCmNsZWFuX3RleHQgPC0gdG9sb3dlcih0ZXh0KSAlPiUKICBzdHJfcmVwbGFjZV9hbGwocGF0dGVybiA9ICJbWzpwdW5jdDpdIF0rIiwgcmVwbGFjZW1lbnQgPSAiICIpICU+JQogIHN0cl90cmltKCkKCndvcmRfZW1iZWRkaW5ncyA8LSBnZXRfZW1iZWRkaW5ncyhjbGVhbl90ZXh0KQpgYGAKCkV4cGxvcmUgeW91ciBvd24gd29yZHMhCgpgYGB7cn0KIyBmaW5kIHdvcmRzIHdpdGggc2ltaWxhciBlbWJlZGRpbmdzCmdldF9zaW1pbGFyX3dvcmRzKCJvaWwiLCB3b3JkX2VtYmVkZGluZ3MpCmBgYAoKIyBQcmVwYXJlIGRhdGEKCk91ciBsYWJlbHMgYXJlIGFscmVhZHkgYSB0ZW5zb3IgKHZlY3Rvcikgc28gd2UgZG9uJ3QgbmVlZCB0byBkbyBhbnkgYWRkaXRpb25hbApwcmVwLgoKYGBge3J9CnN0cihsYWJlbHMpCmBgYAoKIyMgUHJlcHJvY2Vzc2luZyBoeXBlcnBhcmFtZXRlcnMKCkhvd2V2ZXIsIHdlIG5lZWQgdG8gcHJlcHJvY2VzcyBvdXIgdGV4dC4gRmlyc3QsIGxldHMgZGVjaWRlIG9uIHR3byBrZXkgcGFyYW1ldGVycwp0byB1c2Ugd2hlbiBwcmVwcm9jZXNzaW5nIG91ciB0ZXh0OgoKMS4gbnVtYmVyIG9mIG1vc3QgZnJlcXVlbnQgd29yZHMgdXNlZCAoc3RhcnQgd2l0aCAyMDAwMCkKMi4gdGhlIG1heGltdW0gbGVuZ3RoIG9mIG91ciBwcm9jZXNzZWQgdGV4dCAoc3RhcnQgd2l0aCAyMDApCgpUaGVzZSBhcmUgdHdvIGh5cGVycGFyYW1ldGVycyB5b3UgY2FuIGNvbWUgYmFjayB0byBhbmQgY2hhbmdlIGFzIGh5cGVycGFyYW1ldGVycy4KCmBgYHtyfQp0b3Bfbl93b3JkcyA8LSBfX19fX18KbWF4X2xlbiA8LSBfX19fX18KYGBgCgojIyBQcmVwcm9jZXNzaW5nIEZlYXR1cmUgdGV4dAoKTmV4dCwgeW91IG5lZWQgdG8gY3JlYXRlIGFuZCBhcHBseSBhIHRva2VuaXplciB0byB0aGUgdGV4dC4KCmBgYHtyfQp0b2tlbml6ZXIgPC0gdGV4dF90b2tlbml6ZXIobnVtX3dvcmRzID0gdG9wX25fd29yZHMpICU+JSAKICBmaXRfdGV4dF90b2tlbml6ZXIodGV4dCkKCm5hbWVzKHRva2VuaXplcikKYGBgCgpOb3csIGNvbnZlcnQgeW91ciB0ZXh0IHRvIGEgbnVtZXJpY2FsbHkgZW5jb2RlZCBzZXF1ZW5jZS4KCmBgYHtyfQpzZXF1ZW5jZXMgPC0gdGV4dHNfdG9fc2VxdWVuY2VzKHRva2VuaXplciwgdGV4dCkKYGBgCgpgYGB7cn0KIyBUaGUgdmVjdG9yaXplZCBmaXJzdCBpbnN0YW5jZToKc2VxdWVuY2VzW1sxXV0KYGBgCgpSdW4gdGhpcyBjb2RlIGNodW5rIHRvIHNlZSBob3cgeW91ciB0ZXh0IGhhcyBiZWVuIGNvbnZlcnRlZDoKCmBgYHtyfSAKY2F0KGNyYXlvbjo6Ymx1ZSgiT3JpZ2luYWwgdGV4dDpcbiIpKQp0ZXh0W1sxXV0KCmNhdChjcmF5b246OmJsdWUoIlxuUmV2aXNlZCB0ZXh0OlxuIikpCnBhc3RlKHVubGlzdCh0b2tlbml6ZXIkaW5kZXhfd29yZClbc2VxdWVuY2VzW1sxXV1dICwgY29sbGFwc2UgPSAiICIpCmBgYAoKTGFzdCwgd2Ugd2FudCB0byBtYWtlIHN1cmUgb3VyIHNlcXVlbmNlcyAoYWthIGVhY2ggcHJvY2Vzc2VkIHJldmlldykgaXMgb2YgZXF1YWwKbGVuZ3RoLgoKYGBge3J9CmZlYXR1cmVzIDwtIHBhZF9zZXF1ZW5jZXMoc2VxdWVuY2VzLCBtYXhsZW4gPSBtYXhfbGVuKQoKZXhwZWN0X2VxdWFsKG5jb2woZmVhdHVyZXMpLCBtYXhfbGVuKQpgYGAKCk1ha2Ugc3VyZSB0aGF0IHRoZSBudW1iZXIgb2Ygb2JzZXJ2YXRpb25zIGluIHlvdXIgZmVhdHVyZXMgYW5kIGxhYmVscyBhcmUgZXF1YWw6CgpgYGB7cn0KZXhwZWN0X2VxdWFsKG5yb3coZmVhdHVyZXMpLCBsZW5ndGgobGFiZWxzKSkKYGBgCgoKIyBNb2RlbCB0cmFpbmluZwoKQmVmb3JlIHdlIHRyYWluIG91ciBtb2RlbCwgbGV0J3MgZ28gYWhlYWQgYW5kIHJhbmRvbWl6ZSBvdXIgcmV2aWV3IGRhdGEgc28gdGhhdApvdXIgdHJhaW5pbmcgYW5kIHZhbGlkYXRpb24gZGF0YSBwcm9wZXJseSByZXByZXNlbnQgYSBtaXh0dXJlIG9mIHByb2R1Y3RzIGFuZAp1c2Vycy4KCmBgYHtyfQpzZXQuc2VlZCgxMjMpCmluZGV4IDwtIHNhbXBsZSgxOm5yb3coZmVhdHVyZXMpKQpzcGxpdF9wb2ludCA8LSBmbG9vcihsZW5ndGgoaW5kZXgpICogLjMpCnRyYWluX2luZGV4IDwtIGluZGV4WzE6c3BsaXRfcG9pbnRdCnZhbGlkX2luZGV4IDwtIGluZGV4WyhzcGxpdF9wb2ludCArIDEpOmxlbmd0aChpbmRleCldCgpleHBlY3RfZXF1YWwobGVuZ3RoKHRyYWluX2luZGV4KSArIGxlbmd0aCh2YWxpZF9pbmRleCksIGxlbmd0aChpbmRleCkpCgp4X3RyYWluIDwtIGZlYXR1cmVzW3RyYWluX2luZGV4LCBdCnlfdHJhaW4gPC0gbGFiZWxzW3RyYWluX2luZGV4XQoKeF92YWxpZCA8LSBmZWF0dXJlc1t2YWxpZF9pbmRleCwgXQp5X3ZhbGlkIDwtIGxhYmVsc1t2YWxpZF9pbmRleF0KYGBgCgpPaywgc28gYmVmb3JlIHdlIHRyYWluIG91ciBtb2RlbCwgbGV0J3MgZ2V0IGFuIHVuZGVyc3RhbmRpbmcgb2YgYSBiYXNlbGluZQpsb3NzIHNjb3JlIHRoYXQgd2Ugd2FudCB0byBiZWF0LiBUaGUgZWFzaWVzdCBiYXNlbGluZSBpcyB0byBqdXN0IHByZWRpY3QgdGhlCmF2ZXJhZ2Ugb2YgdGhlIHRyYWluaW5nIGxhYmVsIGZvciBmdXR1cmUgb2JzZXJ2YXRpb25zLgoKYGBge3J9CmF2ZyA8LSBtZWFuKHlfdHJhaW4pCmJhc2VsaW5lX21zZSA8LSBtZWFuKCh5X3ZhbGlkIC0gYXZnKV4yKQoKY2F0KCJTaW1wbHkgcHJlZGljdGluZyB0aGUgYXZlcmFnZSBoZWxwZnVsbmVzcyBzY29yZSBvZiIsIHJvdW5kKGF2ZywgMiksCiAgICAiZm9yIGV2ZXJ5IHJldmlldyB3b3VsZCBnaXZlIHVzIGEgbG9zcyBzY29yZSBvZiIsIHJvdW5kKGJhc2VsaW5lX21zZSwgMykpCmBgYAoKT2ssIHRpbWUgdG8gYnVpbGQgeW91ciBtb2RlbCBhcmNoaXRlY3R1cmUgYW5kIGNvbXBpbGUgaXQuIEZpbGwgaW4gdGhlIG1vZGVsaW5nCmJsYW5rcyBhbmQgY29uc2lkZXIgdGhlIGZvbGxvd2luZzoKCjEuIFlvdXIgd29yZCBlbWJlZGRpbmcgYGlucHV0X2RpbWAgd2FzIGFscmVhZHkgZXN0YWJsaXNoZWQgd2l0aCBgdG9wX25fd29yZHNgCiAgIC0gUmVmOiBsaW5lIDIyNgogICAtIGZlZWwgZnJlZSB0byBjaGFuZ2UgdGhpcyB2YWx1ZXMgYW5kIHNlZSBob3cgdGhleSBpbXBhY3QgcGVyZm9ybWFuY2UKMi4gWW91ciB3b3JkIGVtYmVkZGluZyBgaW5wdXRfbGVuZ3RoYCB3YXMgYWxyZWFkeSBlc3RhYmxpc2hlZCB3aXRoIGBtYXhfbGVuYCBvbgogICAtIFJlZjogbGluZSAyMjcKICAgLSBmZWVsIGZyZWUgdG8gY2hhbmdlIHRoaXMgdmFsdWVzIGFuZCBzZWUgaG93IHRoZXkgaW1wYWN0IHBlcmZvcm1hbmNlCjMuIFRyeSBvdXQgZGlmZmVyZW50IGBvdXRwdXRfZGltYCB2YWx1ZXMgZm9yIHRoZSB3b3JkIGVtYmVkZGluZ3MKICAgLSB0eXBpY2FsIHZhbHVlczogcG93ZXJzIG9mIDIgLS0+IDE2LCAzMiwgNjQsIDEyOCwgMjU2CjQuIEZlZWwgZnJlZSB0byBhZGQgYWRkaXRpb25hbCBoaWRkZW4gbGF5ZXJzIGFuZCBkcm9wb3V0IGxheWVycyB0byB0aGUgZGVuc2VseQogICBjb25uZWN0ZWQgY2xhc3NpZmllci4KCmBgYHtyfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgbGF5ZXJfZW1iZWRkaW5nKGlucHV0X2RpbSA9IF9fX19fLCAKICAgICAgICAgICAgICAgICAgb3V0cHV0X2RpbSA9IF9fX19fLAogICAgICAgICAgICAgICAgICBpbnB1dF9sZW5ndGggPSBfX19fXykgJT4lIAogIGxheWVyX2ZsYXR0ZW4oKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEpCgptb2RlbCAlPiUgY29tcGlsZSgKICBvcHRpbWl6ZXIgPSBfX19fXywKICBsb3NzID0gIm1zZSIsCiAgbWV0cmljcyA9IF9fX19fCikKCnN1bW1hcnkobW9kZWwpCmBgYAoKTGV0J3MgdHJhaW4gb3VyIG1vZGVsOgoKYGBge3J9Cmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB4X3RyYWluLCB5X3RyYWluLAogIGVwb2NocyA9IF9fX19fLAogIGJhdGNoX3NpemUgPSBfX19fXywKICB2YWxpZGF0aW9uX2RhdGEgPSBsaXN0KHhfdmFsaWQsIHlfdmFsaWQpLAogIGNhbGxiYWNrcyA9IGxpc3QoCiAgICBjYWxsYmFja19yZWR1Y2VfbHJfb25fcGxhdGVhdShwYXRpZW5jZSA9IF9fX19fKSwKICAgIGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gX19fX18sIHJlc3RvcmVfYmVzdF93ZWlnaHRzID0gVFJVRSkKICAgICkKKQpgYGAKCkxldCdzIGNvbXBhcmUgdGhlIG9wdGltYWwgbG9zcyBzY29yZSB2ZXJzdXMgdGhlIGJhc2VsaW5lIGxvc3Mgc2NvcmUuIAoKYGBge3J9Cm9wdF9tc2UgPC0gbWluKGhpc3RvcnkkbWV0cmljcyR2YWxfbG9zcykKZ2x1ZSgiQmFzZWxpbmUgbG9zcyBzY29yZToge3JvdW5kKGJhc2VsaW5lX21zZSwgMyl9IikKZ2x1ZSgiTW9kZWwgbG9zcyBzY29yZToge3JvdW5kKG9wdF9tc2UsIDMpfSIpCmBgYAoK