In this example, we are going to learn about recurrent neural networks (RNNs) and long short term memory (LSTM) neural networks. These architectures are designed for sequence data, which can include text, videos, time series, and more. However, for this workshop we are going to focus on text data as this is the domain seeing the largest impact from LSTMs.
Learning objectives:
- Understand how an RNN works, how to implement it, and its primary weakness
- Understand how an LSTM works and how to implement it
Requirements
library(keras)
library(tidyverse)
library(glue)
Prepare our data
For our introduction we’re going to start with the built-in IMDB data set. Let’s use the 10,000 most frequent words and we will set the max length of each review to be 500, which will capture the majority of most reviews.
# establish our data characteristics
n_features <- 10000
max_len <- 500
# import and prep our data
imdb <- dataset_imdb(num_words = n_features)
c(c(x_train, y_train), c(x_test, y_test)) %<-% imdb
x_train <- pad_sequences(x_train, maxlen = max_len)
x_test <- pad_sequences(x_test, maxlen = max_len)
RNNs
To model our IMDB data, we could use:
- one-hot encoding, which has a problem with only allowing a single representation of a word. “Seattle” is simply a word but has no relationship to “Seahawks”, “Wilson”, “weather”, or “Starbucks”.
- embeddings, which allow our inputs (i.e. words) to have relationships with other inputs but does not control for the sequential nature of the inputs.
Recurrent neural networks were specifically designed to model sequential data. ℹ️
Train an RNN
Let’s build a model with an RNN layer. First, we still use an embedding layer to allow for more complex representation of our words but we follow that with layer_simple_rnn()
. Everything else remains the same as before.
model <- keras_model_sequential() %>%
layer_embedding(
input_dim = n_features,
input_length = max_len,
output_dim = 32,
name = "Embeddings") %>%
layer_simple_rnn(units = 32, name = "RNN") %>%
layer_dense(units = 1, activation = "sigmoid", name = "Prediction")
model %>% compile(
optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = "accuracy"
)
summary(model)
Model: "sequential_1"
________________________________________________________________________________________
Layer (type) Output Shape Param #
========================================================================================
Embeddings (Embedding) (None, 500, 32) 320000
________________________________________________________________________________________
RNN (SimpleRNN) (None, 32) 2080
________________________________________________________________________________________
Prediction (Dense) (None, 1) 33
========================================================================================
Total params: 322,113
Trainable params: 322,113
Non-trainable params: 0
________________________________________________________________________________________
history <- model %>% fit(
x_train, y_train,
epochs = 10,
batch_size = 128,
validation_split = 0.2,
callbacks = list(callback_early_stopping(patience = 3))
)
best_epoch <- which(history$metrics$val_loss == min(history$metrics$val_loss))
loss <- history$metrics$val_loss[best_epoch] %>% round(3)
acc <- history$metrics$val_acc[best_epoch] %>% round(3)
glue("The best epoch had a loss of {loss} and an accuracy of {acc}")
The best epoch had a loss of 0.36 and an accuracy of 0.852
Your Turn! (5min)
Spend a few minutes adjusting this model and see how it impacts performance. You may want to test:
- Does increasing and decreasing the word embedding dimension and/or the number of RNN layer units impact performance?
- It’s sometimes userful to stack several recurrent layers one after the other in order to increase the representational power of a network. What happens when you try this?
Tip: An RNN will take inputs from different parts of a sequence and produce a single output. When stacking multiple recurrent layers, you need to set return_sequences = TRUE
so that you get the output for each part of the sequence so that you can supply a sequence into the next RNN layer (see ?layer_simple_rnn()
).
model <- keras_model_sequential() %>%
layer_embedding(
input_dim = n_features,
input_length = max_len,
output_dim = 32,
name = "Embeddings") %>%
layer_simple_rnn(units = 32, name = "RNN_1", return_sequences = TRUE) %>%
layer_simple_rnn(units = 32, name = "RNN_2") %>%
layer_dense(units = 1, activation = "sigmoid", name = "Prediction")
model %>% compile(
optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = "accuracy"
)
summary(model)
history <- model %>% fit(
x_train, y_train,
epochs = 10,
batch_size = 128,
validation_split = 0.2,
callbacks = list(callback_early_stopping(patience = _____))
)
best_epoch <- which(history$metrics$val_loss == min(history$metrics$val_loss))
loss <- history$metrics$val_loss[best_epoch] %>% round(3)
acc <- history$metrics$val_acc[best_epoch] %>% round(3)
glue("The best epoch had a loss of {loss} and an accuracy of {acc}")
LSTMs
Unfortunately, a problem with RNNs (and any very deep neural network) is as our network gets deep, it loses the signal due to the vanishing gradient descent problem. LSTMs were developed to address this problem. ℹ️
Train an LSTM
To train an LSTM, we simply replace layer_simple_rnn()
with layer_lstm()
.
model <- keras_model_sequential() %>%
layer_embedding(
input_dim = n_features,
input_length = max_len,
output_dim = 32,
name = "Embeddings") %>%
layer_lstm(units = 32, name = "LSTM") %>%
layer_dense(units = 1, activation = "sigmoid", name = "Prediction")
model %>% compile(
optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = "accuracy"
)
summary(model)
Model: "sequential_2"
________________________________________________________________________________________
Layer (type) Output Shape Param #
========================================================================================
Embeddings (Embedding) (None, 500, 32) 320000
________________________________________________________________________________________
LSTM (LSTM) (None, 32) 8320
________________________________________________________________________________________
Prediction (Dense) (None, 1) 33
========================================================================================
Total params: 328,353
Trainable params: 328,353
Non-trainable params: 0
________________________________________________________________________________________
history <- model %>% fit(
x_train, y_train,
epochs = 10,
batch_size = 128,
validation_split = 0.2,
callbacks = list(callback_early_stopping(patience = 3))
)
best_epoch <- which(history$metrics$val_loss == min(history$metrics$val_loss))
loss <- history$metrics$val_loss[best_epoch] %>% round(3)
acc <- history$metrics$val_acc[best_epoch] %>% round(3)
glue("The best epoch had a loss of {loss} and an accuracy of {acc}")
The best epoch had a loss of 0.31 and an accuracy of 0.885
Your Turn! (5min)
Spend a few minutes adjusting this model and see how it impacts performance. You may want to test:
- Does increasing and decreasing the word embedding dimension and/or the number of LSTM layer units impact performance?
- As we learned in an earlier module, dropout can help reduce overfitting and improve model performance. See if you can add dropout to your model. Hint, checkout the docs (
?layer_lstm()
) because adding dropout to LSTMs are a little unique.
Tip: When applying dropout to a recurrent network, it’s important to apply the same dropout pattern to each timestep within the recurrent layer, rather than randomly. Consequently, recurrent layers have two dropout parameters:
dropout
: the dropout rate for inputs going into the recurrent layer
recurrent_dropout
: the dropout rate for the recurrent units within the recurrent layer
See ?layer_lstm()
for details. Note, you can still apply dropout between the embedding layer and the dense layer after the LSTM layer by using the regular layer_dropout()
layer.
model <- keras_model_sequential() %>%
layer_embedding(
input_dim = n_features,
input_length = max_len,
output_dim = 32,
name = "Embeddings") %>%
layer_lstm(units = 32, dropout = 0.3, recurrent_dropout = 0.3, name = "LSTM") %>%
layer_dense(units = 1, activation = "sigmoid", name = "Prediction")
model %>% compile(
optimizer = "rmsprop",
loss = "binary_crossentropy",
metrics = "accuracy"
)
summary(model)
history <- model %>% fit(
x_train, y_train,
epochs = 10,
batch_size = 128,
validation_split = 0.2,
callbacks = _____
)
best_epoch <- which(history$metrics$val_loss == min(history$metrics$val_loss))
loss <- history$metrics$val_loss[best_epoch] %>% round(3)
acc <- history$metrics$val_acc[best_epoch] %>% round(3)
glue("The best epoch had a loss of {loss} and an accuracy of {acc}")
Why the lack of accuracy
So why are our recurrent models not doing much better than our one-hot encoding and simple word embedding models? For most sentiment predictions we can capture the essence of the tone (i.e. positive vs. negative) by focusing on a few key words, which our simpler encoding models can do quite well.
As your prediction task requires more spatial context, then recurrent networks should begin to out perform the more simpler encoding models.
Takeaways
- Recurrent networks help to capture spatial context within text
- RNNs
- are just multiple perceptrons sequentially linked
- pass information through hidden states
- use the Tanh activation to control value magnitudes
- suffer from the vanishing gradient
- implemented with
layer_simple_rnn()
- LSTMs
- use multiple gates that help to “remember” or “forget” information
- computationally expensive
- implemented with
layer_lstm()
- Unique characteristics
- must use
return_sequences = TRUE
to stack multiple recurrent layers
- recurrent layers have their own special dropout procedures –> don’t use
layer_dropout()
; rather, use the dropout parameters within the recurrent layer.
🏠
LS0tCnRpdGxlOiAiSW50cm9kdWN0aW9uIHRvIFJOTnMgJiBMU1RNcyIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB0cnVlCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0UpCmdncGxvdDI6OnRoZW1lX3NldChnZ3Bsb3QyOjp0aGVtZV9idygpKQpgYGAKCkluIHRoaXMgZXhhbXBsZSwgd2UgYXJlIGdvaW5nIHRvIGxlYXJuIGFib3V0IHJlY3VycmVudCBuZXVyYWwgbmV0d29ya3MgKFJOTnMpCmFuZCBsb25nIHNob3J0IHRlcm0gbWVtb3J5IChMU1RNKSBuZXVyYWwgbmV0d29ya3MuIFRoZXNlIGFyY2hpdGVjdHVyZXMgYXJlCmRlc2lnbmVkIGZvciBzZXF1ZW5jZSBkYXRhLCB3aGljaCBjYW4gaW5jbHVkZSB0ZXh0LCB2aWRlb3MsIHRpbWUgc2VyaWVzLCBhbmQKbW9yZS4gSG93ZXZlciwgZm9yIHRoaXMgd29ya3Nob3Agd2UgYXJlIGdvaW5nIHRvIGZvY3VzIG9uIHRleHQgZGF0YSBhcyB0aGlzIGlzCnRoZSBkb21haW4gc2VlaW5nIHRoZSBsYXJnZXN0IGltcGFjdCBmcm9tIExTVE1zLgoKTGVhcm5pbmcgb2JqZWN0aXZlczoKCi0gVW5kZXJzdGFuZCBob3cgYW4gUk5OIHdvcmtzLCBob3cgdG8gaW1wbGVtZW50IGl0LCBhbmQgaXRzIHByaW1hcnkgd2Vha25lc3MKLSBVbmRlcnN0YW5kIGhvdyBhbiBMU1RNIHdvcmtzIGFuZCBob3cgdG8gaW1wbGVtZW50IGl0CgojIFJlcXVpcmVtZW50cwoKYGBge3J9CmxpYnJhcnkoa2VyYXMpCmxpYnJhcnkodGlkeXZlcnNlKQpsaWJyYXJ5KGdsdWUpCmBgYAoKIyBQcmVwYXJlIG91ciBkYXRhCgpGb3Igb3VyIGludHJvZHVjdGlvbiB3ZSdyZSBnb2luZyB0byBzdGFydCB3aXRoIHRoZSBidWlsdC1pbiBJTURCIGRhdGEgc2V0LiBMZXQncwp1c2UgdGhlIDEwLDAwMCBtb3N0IGZyZXF1ZW50IHdvcmRzIGFuZCB3ZSB3aWxsIHNldCB0aGUgbWF4IGxlbmd0aCBvZiBlYWNoCnJldmlldyB0byBiZSA1MDAsIHdoaWNoIHdpbGwgY2FwdHVyZSB0aGUgbWFqb3JpdHkgb2YgbW9zdCByZXZpZXdzLgoKYGBge3J9CiMgZXN0YWJsaXNoIG91ciBkYXRhIGNoYXJhY3RlcmlzdGljcwpuX2ZlYXR1cmVzIDwtIDEwMDAwCm1heF9sZW4gPC0gNTAwCgojIGltcG9ydCBhbmQgcHJlcCBvdXIgZGF0YQppbWRiIDwtIGRhdGFzZXRfaW1kYihudW1fd29yZHMgPSBuX2ZlYXR1cmVzKQpjKGMoeF90cmFpbiwgeV90cmFpbiksIGMoeF90ZXN0LCB5X3Rlc3QpKSAlPC0lIGltZGIKCnhfdHJhaW4gPC0gcGFkX3NlcXVlbmNlcyh4X3RyYWluLCBtYXhsZW4gPSBtYXhfbGVuKQp4X3Rlc3QgPC0gcGFkX3NlcXVlbmNlcyh4X3Rlc3QsIG1heGxlbiA9IG1heF9sZW4pCmBgYAoKIyBSTk5zCgpUbyBtb2RlbCBvdXIgSU1EQiBkYXRhLCB3ZSBjb3VsZCB1c2U6CgoxLiBfX29uZS1ob3QgZW5jb2RpbmdfXywgd2hpY2ggaGFzIGEgcHJvYmxlbSB3aXRoIG9ubHkgYWxsb3dpbmcgYSBzaW5nbGUKICAgcmVwcmVzZW50YXRpb24gb2YgYSB3b3JkLiAiU2VhdHRsZSIgaXMgc2ltcGx5IGEgd29yZCBidXQgaGFzIG5vIHJlbGF0aW9uc2hpcAogICB0byAiU2VhaGF3a3MiLCAiV2lsc29uIiwgIndlYXRoZXIiLCBvciAiU3RhcmJ1Y2tzIi4KMi4gX19lbWJlZGRpbmdzX18sIHdoaWNoIGFsbG93IG91ciBpbnB1dHMgKGkuZS4gd29yZHMpIHRvIGhhdmUgcmVsYXRpb25zaGlwcwogICB3aXRoIG90aGVyIGlucHV0cyBidXQgZG9lcyBub3QgY29udHJvbCBmb3IgdGhlIHNlcXVlbnRpYWwgbmF0dXJlIG9mIHRoZQogICBpbnB1dHMuCiAgIApSZWN1cnJlbnQgbmV1cmFsIG5ldHdvcmtzIHdlcmUgc3BlY2lmaWNhbGx5IGRlc2lnbmVkIHRvIG1vZGVsIHNlcXVlbnRpYWwgZGF0YS4KW+KEue+4j10oaHR0cDovL2JpdC5seS9kbC0wOCkKCiMjIFRyYWluIGFuIFJOTgoKTGV0J3MgYnVpbGQgYSBtb2RlbCB3aXRoIGFuIFJOTiBsYXllci4gRmlyc3QsIHdlIHN0aWxsIHVzZSBhbiBlbWJlZGRpbmcgbGF5ZXIgdG8KYWxsb3cgZm9yIG1vcmUgY29tcGxleCByZXByZXNlbnRhdGlvbiBvZiBvdXIgd29yZHMgYnV0IHdlIGZvbGxvdyB0aGF0IHdpdGgKYGxheWVyX3NpbXBsZV9ybm4oKWAuIEV2ZXJ5dGhpbmcgZWxzZSByZW1haW5zIHRoZSBzYW1lIGFzIGJlZm9yZS4KCmBgYHtyfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgbGF5ZXJfZW1iZWRkaW5nKAogICAgaW5wdXRfZGltID0gbl9mZWF0dXJlcywKICAgIGlucHV0X2xlbmd0aCA9IG1heF9sZW4sCiAgICBvdXRwdXRfZGltID0gMzIsIAogICAgbmFtZSA9ICJFbWJlZGRpbmdzIikgJT4lCiAgbGF5ZXJfc2ltcGxlX3Jubih1bml0cyA9IDMyLCBuYW1lID0gIlJOTiIpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSwgYWN0aXZhdGlvbiA9ICJzaWdtb2lkIiwgbmFtZSA9ICJQcmVkaWN0aW9uIikKCm1vZGVsICU+JSBjb21waWxlKAogIG9wdGltaXplciA9ICJybXNwcm9wIiwKICBsb3NzID0gImJpbmFyeV9jcm9zc2VudHJvcHkiLAogIG1ldHJpY3MgPSAiYWNjdXJhY3kiCiAgKQoKc3VtbWFyeShtb2RlbCkKYGBgCgpgYGB7cn0KaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KAogIHhfdHJhaW4sIHlfdHJhaW4sCiAgZXBvY2hzID0gMTAsCiAgYmF0Y2hfc2l6ZSA9IDEyOCwKICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4yLAogIGNhbGxiYWNrcyA9IGxpc3QoY2FsbGJhY2tfZWFybHlfc3RvcHBpbmcocGF0aWVuY2UgPSAzKSkKKQpgYGAKCmBgYHtyfQpiZXN0X2Vwb2NoIDwtIHdoaWNoKGhpc3RvcnkkbWV0cmljcyR2YWxfbG9zcyA9PSBtaW4oaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzKSkKbG9zcyA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3NbYmVzdF9lcG9jaF0gJT4lIHJvdW5kKDMpCmFjYyA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX2FjY1tiZXN0X2Vwb2NoXSAlPiUgcm91bmQoMykKCmdsdWUoIlRoZSBiZXN0IGVwb2NoIGhhZCBhIGxvc3Mgb2Yge2xvc3N9IGFuZCBhbiBhY2N1cmFjeSBvZiB7YWNjfSIpCmBgYAoKIyMgWW91ciBUdXJuISAoNW1pbikKClNwZW5kIGEgZmV3IG1pbnV0ZXMgYWRqdXN0aW5nIHRoaXMgbW9kZWwgYW5kIHNlZSBob3cgaXQgaW1wYWN0cyBwZXJmb3JtYW5jZS4gWW91Cm1heSB3YW50IHRvIHRlc3Q6CgotIERvZXMgaW5jcmVhc2luZyBhbmQgZGVjcmVhc2luZyB0aGUgd29yZCBlbWJlZGRpbmcgZGltZW5zaW9uIGFuZC9vciB0aGUgbnVtYmVyCiAgb2YgUk5OIGxheWVyIHVuaXRzIGltcGFjdCBwZXJmb3JtYW5jZT8KLSBJdCdzIHNvbWV0aW1lcyB1c2VyZnVsIHRvIHN0YWNrIHNldmVyYWwgcmVjdXJyZW50IGxheWVycyBvbmUgYWZ0ZXIgdGhlIG90aGVyCiAgaW4gb3JkZXIgdG8gaW5jcmVhc2UgdGhlIHJlcHJlc2VudGF0aW9uYWwgcG93ZXIgb2YgYSBuZXR3b3JrLiBXaGF0IGhhcHBlbnMKICB3aGVuIHlvdSB0cnkgdGhpcz8KICAKX19UaXBfXzogQW4gUk5OIHdpbGwgdGFrZSBpbnB1dHMgZnJvbSBkaWZmZXJlbnQgcGFydHMgb2YgYSBzZXF1ZW5jZSBhbmQgcHJvZHVjZQphIHNpbmdsZSBvdXRwdXQuIFdoZW4gc3RhY2tpbmcgbXVsdGlwbGUgcmVjdXJyZW50IGxheWVycywgeW91IG5lZWQgdG8gc2V0CmByZXR1cm5fc2VxdWVuY2VzID0gVFJVRWAgc28gdGhhdCB5b3UgZ2V0IHRoZSBvdXRwdXQgZm9yIGVhY2ggcGFydCBvZiB0aGUKc2VxdWVuY2Ugc28gdGhhdCB5b3UgY2FuIHN1cHBseSBhIHNlcXVlbmNlIGludG8gdGhlIG5leHQgUk5OIGxheWVyIChzZWUKYD9sYXllcl9zaW1wbGVfcm5uKClgKS4KICAKYGBge3J9Cm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUKICBsYXllcl9lbWJlZGRpbmcoCiAgICBpbnB1dF9kaW0gPSBuX2ZlYXR1cmVzLAogICAgaW5wdXRfbGVuZ3RoID0gbWF4X2xlbiwKICAgIG91dHB1dF9kaW0gPSAzMiwgCiAgICBuYW1lID0gIkVtYmVkZGluZ3MiKSAlPiUKICBsYXllcl9zaW1wbGVfcm5uKHVuaXRzID0gMzIsIG5hbWUgPSAiUk5OXzEiLCByZXR1cm5fc2VxdWVuY2VzID0gVFJVRSkgJT4lCiAgbGF5ZXJfc2ltcGxlX3Jubih1bml0cyA9IDMyLCBuYW1lID0gIlJOTl8yIikgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxLCBhY3RpdmF0aW9uID0gInNpZ21vaWQiLCBuYW1lID0gIlByZWRpY3Rpb24iKQoKbW9kZWwgJT4lIGNvbXBpbGUoCiAgb3B0aW1pemVyID0gInJtc3Byb3AiLAogIGxvc3MgPSAiYmluYXJ5X2Nyb3NzZW50cm9weSIsCiAgbWV0cmljcyA9ICJhY2N1cmFjeSIKICApCgpzdW1tYXJ5KG1vZGVsKQpgYGAKCmBgYHtyfQpoaXN0b3J5IDwtIG1vZGVsICU+JSBmaXQoCiAgeF90cmFpbiwgeV90cmFpbiwKICBlcG9jaHMgPSAxMCwKICBiYXRjaF9zaXplID0gMTI4LAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIsCiAgY2FsbGJhY2tzID0gbGlzdChjYWxsYmFja19lYXJseV9zdG9wcGluZyhwYXRpZW5jZSA9IF9fX19fKSkKKQpgYGAKCmBgYHtyfQpiZXN0X2Vwb2NoIDwtIHdoaWNoKGhpc3RvcnkkbWV0cmljcyR2YWxfbG9zcyA9PSBtaW4oaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzKSkKbG9zcyA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3NbYmVzdF9lcG9jaF0gJT4lIHJvdW5kKDMpCmFjYyA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX2FjY1tiZXN0X2Vwb2NoXSAlPiUgcm91bmQoMykKCmdsdWUoIlRoZSBiZXN0IGVwb2NoIGhhZCBhIGxvc3Mgb2Yge2xvc3N9IGFuZCBhbiBhY2N1cmFjeSBvZiB7YWNjfSIpCmBgYAoKIyBMU1RNcwoKVW5mb3J0dW5hdGVseSwgYSBwcm9ibGVtIHdpdGggUk5OcyAoYW5kIGFueSB2ZXJ5IGRlZXAgbmV1cmFsIG5ldHdvcmspIGlzIGFzIG91cgpuZXR3b3JrIGdldHMgZGVlcCwgaXQgbG9zZXMgdGhlIHNpZ25hbCBkdWUgdG8gdGhlIHZhbmlzaGluZyBncmFkaWVudCBkZXNjZW50CnByb2JsZW0uIExTVE1zIHdlcmUgZGV2ZWxvcGVkIHRvIGFkZHJlc3MgdGhpcyBwcm9ibGVtLiBb4oS577iPXShodHRwOi8vYml0Lmx5L2RsLTA4IzEyKQoKIyMgVHJhaW4gYW4gTFNUTQoKVG8gdHJhaW4gYW4gTFNUTSwgd2Ugc2ltcGx5IHJlcGxhY2UgYGxheWVyX3NpbXBsZV9ybm4oKWAgd2l0aCBgbGF5ZXJfbHN0bSgpYC4KCmBgYHtyfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgbGF5ZXJfZW1iZWRkaW5nKAogICAgaW5wdXRfZGltID0gbl9mZWF0dXJlcywKICAgIGlucHV0X2xlbmd0aCA9IG1heF9sZW4sCiAgICBvdXRwdXRfZGltID0gMzIsIAogICAgbmFtZSA9ICJFbWJlZGRpbmdzIikgJT4lCiAgbGF5ZXJfbHN0bSh1bml0cyA9IDMyLCBuYW1lID0gIkxTVE0iKSAlPiUKICBsYXllcl9kZW5zZSh1bml0cyA9IDEsIGFjdGl2YXRpb24gPSAic2lnbW9pZCIsIG5hbWUgPSAiUHJlZGljdGlvbiIpCgptb2RlbCAlPiUgY29tcGlsZSgKICBvcHRpbWl6ZXIgPSAicm1zcHJvcCIsCiAgbG9zcyA9ICJiaW5hcnlfY3Jvc3NlbnRyb3B5IiwKICBtZXRyaWNzID0gImFjY3VyYWN5IgogICkKCnN1bW1hcnkobW9kZWwpCmBgYAoKYGBge3J9Cmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB4X3RyYWluLCB5X3RyYWluLAogIGVwb2NocyA9IDEwLAogIGJhdGNoX3NpemUgPSAxMjgsCiAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMiwKICBjYWxsYmFja3MgPSBsaXN0KGNhbGxiYWNrX2Vhcmx5X3N0b3BwaW5nKHBhdGllbmNlID0gMykpCikKYGBgCgpgYGB7cn0KYmVzdF9lcG9jaCA8LSB3aGljaChoaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3MgPT0gbWluKGhpc3RvcnkkbWV0cmljcyR2YWxfbG9zcykpCmxvc3MgPC0gaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzW2Jlc3RfZXBvY2hdICU+JSByb3VuZCgzKQphY2MgPC0gaGlzdG9yeSRtZXRyaWNzJHZhbF9hY2NbYmVzdF9lcG9jaF0gJT4lIHJvdW5kKDMpCgpnbHVlKCJUaGUgYmVzdCBlcG9jaCBoYWQgYSBsb3NzIG9mIHtsb3NzfSBhbmQgYW4gYWNjdXJhY3kgb2Yge2FjY30iKQpgYGAKCiMjIFlvdXIgVHVybiEgKDVtaW4pCgpTcGVuZCBhIGZldyBtaW51dGVzIGFkanVzdGluZyB0aGlzIG1vZGVsIGFuZCBzZWUgaG93IGl0IGltcGFjdHMgcGVyZm9ybWFuY2UuIFlvdQptYXkgd2FudCB0byB0ZXN0OgoKLSBEb2VzIGluY3JlYXNpbmcgYW5kIGRlY3JlYXNpbmcgdGhlIHdvcmQgZW1iZWRkaW5nIGRpbWVuc2lvbiBhbmQvb3IgdGhlIG51bWJlcgogIG9mIExTVE0gbGF5ZXIgdW5pdHMgaW1wYWN0IHBlcmZvcm1hbmNlPwotIEFzIHdlIGxlYXJuZWQgaW4gYW4gZWFybGllciBtb2R1bGUsIGRyb3BvdXQgY2FuIGhlbHAgcmVkdWNlIG92ZXJmaXR0aW5nIGFuZAogIGltcHJvdmUgbW9kZWwgcGVyZm9ybWFuY2UuIFNlZSBpZiB5b3UgY2FuIGFkZCBkcm9wb3V0IHRvIHlvdXIgbW9kZWwuIEhpbnQsCiAgY2hlY2tvdXQgdGhlIGRvY3MgKGA/bGF5ZXJfbHN0bSgpYCkgYmVjYXVzZSBhZGRpbmcgZHJvcG91dCB0byBMU1RNcyBhcmUgYQogIGxpdHRsZSB1bmlxdWUuCgpfX1RpcF9fOiBXaGVuIGFwcGx5aW5nIGRyb3BvdXQgdG8gYSByZWN1cnJlbnQgbmV0d29yaywgaXTigJlzIGltcG9ydGFudCB0byBhcHBseQp0aGUgc2FtZSBkcm9wb3V0IHBhdHRlcm4gdG8gZWFjaCB0aW1lc3RlcCB3aXRoaW4gdGhlIHJlY3VycmVudCBsYXllciwgcmF0aGVyCnRoYW4gcmFuZG9tbHkuIENvbnNlcXVlbnRseSwgcmVjdXJyZW50IGxheWVycyBoYXZlIHR3byBkcm9wb3V0IHBhcmFtZXRlcnM6CgotIGBkcm9wb3V0YDogdGhlIGRyb3BvdXQgcmF0ZSBmb3IgaW5wdXRzIGdvaW5nIGludG8gdGhlIHJlY3VycmVudCBsYXllcgotIGByZWN1cnJlbnRfZHJvcG91dGA6IHRoZSBkcm9wb3V0IHJhdGUgZm9yIHRoZSByZWN1cnJlbnQgdW5pdHMgd2l0aGluIHRoZQogICByZWN1cnJlbnQgbGF5ZXIKICAgClNlZSBgP2xheWVyX2xzdG0oKWAgZm9yIGRldGFpbHMuIE5vdGUsIHlvdSBjYW4gc3RpbGwgYXBwbHkgZHJvcG91dCBiZXR3ZWVuIHRoZQplbWJlZGRpbmcgbGF5ZXIgYW5kIHRoZSBkZW5zZSBsYXllciBhZnRlciB0aGUgTFNUTSBsYXllciBieSB1c2luZyB0aGUgcmVndWxhcgpgbGF5ZXJfZHJvcG91dCgpYCBsYXllci4KCmBgYHtyfQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lCiAgbGF5ZXJfZW1iZWRkaW5nKAogICAgaW5wdXRfZGltID0gbl9mZWF0dXJlcywKICAgIGlucHV0X2xlbmd0aCA9IG1heF9sZW4sCiAgICBvdXRwdXRfZGltID0gMzIsIAogICAgbmFtZSA9ICJFbWJlZGRpbmdzIikgJT4lCiAgbGF5ZXJfbHN0bSh1bml0cyA9IDMyLCBkcm9wb3V0ID0gMC4zLCByZWN1cnJlbnRfZHJvcG91dCA9IDAuMywgbmFtZSA9ICJMU1RNIikgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxLCBhY3RpdmF0aW9uID0gInNpZ21vaWQiLCBuYW1lID0gIlByZWRpY3Rpb24iKQoKbW9kZWwgJT4lIGNvbXBpbGUoCiAgb3B0aW1pemVyID0gInJtc3Byb3AiLAogIGxvc3MgPSAiYmluYXJ5X2Nyb3NzZW50cm9weSIsCiAgbWV0cmljcyA9ICJhY2N1cmFjeSIKICApCgpzdW1tYXJ5KG1vZGVsKQpgYGAKCmBgYHtyfQpoaXN0b3J5IDwtIG1vZGVsICU+JSBmaXQoCiAgeF90cmFpbiwgeV90cmFpbiwKICBlcG9jaHMgPSAxMCwKICBiYXRjaF9zaXplID0gMTI4LAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIsCiAgY2FsbGJhY2tzID0gX19fX18KKQpgYGAKCmBgYHtyfQpiZXN0X2Vwb2NoIDwtIHdoaWNoKGhpc3RvcnkkbWV0cmljcyR2YWxfbG9zcyA9PSBtaW4oaGlzdG9yeSRtZXRyaWNzJHZhbF9sb3NzKSkKbG9zcyA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX2xvc3NbYmVzdF9lcG9jaF0gJT4lIHJvdW5kKDMpCmFjYyA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX2FjY1tiZXN0X2Vwb2NoXSAlPiUgcm91bmQoMykKCmdsdWUoIlRoZSBiZXN0IGVwb2NoIGhhZCBhIGxvc3Mgb2Yge2xvc3N9IGFuZCBhbiBhY2N1cmFjeSBvZiB7YWNjfSIpCmBgYAoKIyBXaHkgdGhlIGxhY2sgb2YgYWNjdXJhY3kKClNvIHdoeSBhcmUgb3VyIHJlY3VycmVudCBtb2RlbHMgbm90IGRvaW5nIG11Y2ggYmV0dGVyIHRoYW4gb3VyIG9uZS1ob3QgZW5jb2RpbmcKYW5kIHNpbXBsZSB3b3JkIGVtYmVkZGluZyBtb2RlbHM/IEZvciBtb3N0IHNlbnRpbWVudCBwcmVkaWN0aW9ucyB3ZSBjYW4gY2FwdHVyZQp0aGUgZXNzZW5jZSBvZiB0aGUgdG9uZSAoaS5lLiBwb3NpdGl2ZSB2cy4gbmVnYXRpdmUpIGJ5IGZvY3VzaW5nIG9uIGEgZmV3IGtleQp3b3Jkcywgd2hpY2ggb3VyIHNpbXBsZXIgZW5jb2RpbmcgbW9kZWxzIGNhbiBkbyBxdWl0ZSB3ZWxsLgoKIVtdKGltYWdlcy9jdXN0b21lcl9yZXZpZXcuZ2lmKQoKQXMgeW91ciBwcmVkaWN0aW9uIHRhc2sgcmVxdWlyZXMgbW9yZSBzcGF0aWFsIGNvbnRleHQsIHRoZW4gcmVjdXJyZW50IG5ldHdvcmtzCnNob3VsZCBiZWdpbiB0byBvdXQgcGVyZm9ybSB0aGUgbW9yZSBzaW1wbGVyIGVuY29kaW5nIG1vZGVscy4KCiMgVGFrZWF3YXlzCgoqIFJlY3VycmVudCBuZXR3b3JrcyBoZWxwIHRvIGNhcHR1cmUgc3BhdGlhbCBjb250ZXh0IHdpdGhpbiB0ZXh0CiogUk5OcwogICAtIGFyZSBqdXN0IG11bHRpcGxlIHBlcmNlcHRyb25zIHNlcXVlbnRpYWxseSBsaW5rZWQKICAgLSBwYXNzIGluZm9ybWF0aW9uIHRocm91Z2ggaGlkZGVuIHN0YXRlcwogICAtIHVzZSB0aGUgVGFuaCBhY3RpdmF0aW9uIHRvIGNvbnRyb2wgdmFsdWUgbWFnbml0dWRlcwogICAtIHN1ZmZlciBmcm9tIHRoZSB2YW5pc2hpbmcgZ3JhZGllbnQKICAgLSBpbXBsZW1lbnRlZCB3aXRoIGBsYXllcl9zaW1wbGVfcm5uKClgCiogTFNUTXMKICAgLSB1c2UgbXVsdGlwbGUgZ2F0ZXMgdGhhdCBoZWxwIHRvICJyZW1lbWJlciIgb3IgImZvcmdldCIgaW5mb3JtYXRpb24KICAgLSBjb21wdXRhdGlvbmFsbHkgZXhwZW5zaXZlCiAgIC0gaW1wbGVtZW50ZWQgd2l0aCBgbGF5ZXJfbHN0bSgpYAoqIFVuaXF1ZSBjaGFyYWN0ZXJpc3RpY3MKICAgLSBtdXN0IHVzZSBgcmV0dXJuX3NlcXVlbmNlcyA9IFRSVUVgIHRvIHN0YWNrIG11bHRpcGxlIHJlY3VycmVudCBsYXllcnMKICAgLSByZWN1cnJlbnQgbGF5ZXJzIGhhdmUgdGhlaXIgb3duIHNwZWNpYWwgZHJvcG91dCBwcm9jZWR1cmVzIC0tPiBkb24ndCB1c2UKICAgICBgbGF5ZXJfZHJvcG91dCgpYDsgcmF0aGVyLCB1c2UgdGhlIGRyb3BvdXQgcGFyYW1ldGVycyB3aXRoaW4gdGhlIHJlY3VycmVudAogICAgIGxheWVyLgoKW/Cfj6BdKGh0dHBzOi8vZ2l0aHViLmNvbS9yc3R1ZGlvLWNvbmYtMjAyMC9kbC1rZXJhcy10Zik=