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)
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:
- number of most frequent words used (start with 20000)
- 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:
- 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
- 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
- Try out different
output_dim
values for the word embeddings
- typical values: powers of 2 –> 16, 32, 64, 128, 256
- 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