In this example, we are going to revisit the MNIST data set but use a CNN to classify the digits. This will take us one step closer to image classification and you will learn the fundamental concepts behind CNNs. ℹ️

Learning objectives:

  • Why is translation invariant & spatial hierarchy important
  • What the general structure of CNN models looks like
  • What is the convolution operation
  • What are feature maps
  • How pooling helps by downsampling

Required packages

library(keras)

Prepare data

Let’s import our training and test data. Rather than turn our image data into a 2D tensor as we did in the earlier module, here we convert our data to a 4D tensor that has:

  • 60K samples (train) and 10K samples (test)
  • height x width = 28x28 pixels
  • 1 color channel (these are gray scale rather than RGB, which has 3 color channels)

As before, our pixels range from 0-255 so we re-scale them to be between 0-1.

mnist <- dataset_mnist()
c(c(train_images, train_labels), c(test_images, test_labels)) %<-% mnist

train_images <- array_reshape(train_images, c(60000, 28, 28, 1)) / 255
test_images <- array_reshape(test_images, c(10000, 28, 28, 1)) / 255

train_labels <- to_categorical(train_labels)
test_labels <- to_categorical(test_labels)

CNN: Feature detector

To run a CNN we will follow a very similar approach to what we’ve seen so far. The main difference is that we create a convolution and max pooling procedure prior to our densley connected MLP. This is known as our feature detector step.

We’ll discuss the details of these steps shortly but for now just realize these main points:

  1. our input_shape is 28x28 image with 1 color channel,
  2. the output of each layer_conv2d() and layer_max_pooling_2d() is a 3D tensor of shape (height, width, channels),
  3. the height and width dimensions tend to shrink as you go deeper in the network,
  4. while the number of channels increase.
model <- keras_model_sequential() %>%
  
  layer_conv_2d(filters = 32, kernel_size = c(3, 3), activation = "relu", 
                input_shape = c(28, 28, 1)) %>%
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu") %>%
  layer_max_pooling_2d(pool_size = c(2, 2)) %>%
  
  layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = "relu")

summary(model)
Model: "sequential_2"
______________________________________________________________________________________
Layer (type)                          Output Shape                       Param #      
======================================================================================
conv2d_6 (Conv2D)                     (None, 26, 26, 32)                 320          
______________________________________________________________________________________
max_pooling2d_4 (MaxPooling2D)        (None, 13, 13, 32)                 0            
______________________________________________________________________________________
conv2d_7 (Conv2D)                     (None, 11, 11, 64)                 18496        
______________________________________________________________________________________
max_pooling2d_5 (MaxPooling2D)        (None, 5, 5, 64)                   0            
______________________________________________________________________________________
conv2d_8 (Conv2D)                     (None, 3, 3, 64)                   36928        
======================================================================================
Total params: 55,744
Trainable params: 55,744
Non-trainable params: 0
______________________________________________________________________________________

CNN: Classifier

Next, we feed the last output tensor of shape (3, 3, 64) into a densely connected MLP. This MLP is to classify our images and we often refer to this part of our CNN as the classifier. The only new concept here is layer_flatten() which is reducing the 3D tensor for a given image to a 1D tensor.

model %>%
  layer_flatten() %>%
  layer_dense(units = 64, activation = "relu") %>%
  layer_dense(units = 10, activation = "softmax")

summary(model)
Model: "sequential_2"
______________________________________________________________________________________
Layer (type)                          Output Shape                       Param #      
======================================================================================
conv2d_6 (Conv2D)                     (None, 26, 26, 32)                 320          
______________________________________________________________________________________
max_pooling2d_4 (MaxPooling2D)        (None, 13, 13, 32)                 0            
______________________________________________________________________________________
conv2d_7 (Conv2D)                     (None, 11, 11, 64)                 18496        
______________________________________________________________________________________
max_pooling2d_5 (MaxPooling2D)        (None, 5, 5, 64)                   0            
______________________________________________________________________________________
conv2d_8 (Conv2D)                     (None, 3, 3, 64)                   36928        
______________________________________________________________________________________
flatten_1 (Flatten)                   (None, 576)                        0            
______________________________________________________________________________________
dense_2 (Dense)                       (None, 64)                         36928        
______________________________________________________________________________________
dense_3 (Dense)                       (None, 10)                         650          
======================================================================================
Total params: 93,322
Trainable params: 93,322
Non-trainable params: 0
______________________________________________________________________________________

CNN: Compile & train

These steps are the same as before. However, you will notice that training takes longer, which is due to the added CNN procedure. While this model is training, let’s discuss what’s happening under the hood of a CNN ℹ️.

Although we used a fairly basic model without optimizing the learning rate, model capacity, batch, etc., you will also notice that our model performance is superior to our MLP model from the earlier module:

  • MLP: loss (~ 0.07) & accuracy (~ 0.975)
  • CNN: loss (~ 0.04) & accuracy (~ 0.99)
model %>% compile(
  optimizer = "rmsprop",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)

history <- model %>% fit(
  train_images, train_labels,
  epochs = 5, 
  batch_size = 128,
  validation_split = 0.2
)
history
Trained on 48,000 samples (batch_size=64, epochs=5)
Final epoch (plot to see history):
        loss: 0.02104
    accuracy: 0.9935
    val_loss: 0.04014
val_accuracy: 0.9889 
plot(history)

Evaluation

Using our CNN we obtain a test set accuracy of ~ 0.99.

model %>% evaluate(test_images, test_labels, verbose = FALSE)
$loss
[1] 0.03451437

$accuracy
[1] 0.9896

Your Turn! (10 min)

Spend 10 minutes adjusting various CNN components:

  • Change the number of filters
  • Change filter/kernel size
  • Adjust the stride
  • Add padding
  • Add more convolution layers

Or keep the same CNN components as above but apply some of the tuning steps we covered this morning:

  • Try different adaptive learning rate optimizers and learning rate values
  • How does batch size impact performance
  • You can even try to add weight decay or dropout to each layer to control overfitting:
    • weight decays can be applied with kernel_regularizer within layer_conv_2d
    • layer_dropout() can be applied before or after pooling but is more commonly seen after. Note that dropout in CNNs will randomly drop out entire feature maps
model <- keras_model_sequential() %>%
  
  layer_conv_2d(filters = ____, kernel_size = ____, activation = "relu", 
                input_shape = c(28, 28, 1)) %>%
  layer_max_pooling_2d(pool_size = ____) %>%
  
  layer_conv_2d(filters = ____, kernel_size = ____, activation = "relu") %>%
  layer_max_pooling_2d(pool_size = ____) %>%
  
  layer_conv_2d(filters = ____, kernel_size = ____, activation = "relu")

model %>%
  _______ %>%
  layer_dense(units = 64, activation = "relu") %>%
  layer_dense(units = 10, activation = "softmax")

model %>% compile(
  optimizer = "rmsprop",
  loss = "categorical_crossentropy",
  metrics = c("accuracy")
)

summary(model)
history <- model %>% fit(
  train_images, train_labels,
  epochs = 5, 
  batch_size = 64,
  validation_split = 0.2
)

Key takeaways

  • CNNs allow us to capture and control for image variance

  • The convolution layer provides the main mechanism for feature engineering
    • We slide a filter/kernel over our images to create feature maps
    • We can use striding and padding to control the size of our feature maps
    • Apply pooling to downsample
  • Always flatten the output of the convolution layer to feed into a dense layer

  • Since all our feature engineering occurs in the convolution layer, we typically need only one hidden dense layer with far fewer units and epochs to train our model

🏠

LS0tCnRpdGxlOiAiQ29tcHV0ZXIgVmlzaW9uOiBNTklTVCByZXZpc3RlZCBhcyBhIENOTiIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICB0b2M6IHllcwogICAgdG9jX2Zsb2F0OiB0cnVlCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nID0gRkFMU0UpCmdncGxvdDI6OnRoZW1lX3NldChnZ3Bsb3QyOjp0aGVtZV9taW5pbWFsKCkpCmBgYAoKSW4gdGhpcyBleGFtcGxlLCB3ZSBhcmUgZ29pbmcgdG8gcmV2aXNpdCB0aGUgTU5JU1QgZGF0YSBzZXQgYnV0IHVzZSBhIENOTiB0byAKY2xhc3NpZnkgdGhlIGRpZ2l0cy4gVGhpcyB3aWxsIHRha2UgdXMgb25lIHN0ZXAgY2xvc2VyIHRvIGltYWdlIGNsYXNzaWZpY2F0aW9uIAphbmQgeW91IHdpbGwgbGVhcm4gdGhlIGZ1bmRhbWVudGFsIGNvbmNlcHRzIGJlaGluZCBDTk5zLiBb4oS577iPXShodHRwOi8vYml0Lmx5L2RsLTAzKQoKTGVhcm5pbmcgb2JqZWN0aXZlczoKCi0gV2h5IGlzIHRyYW5zbGF0aW9uIGludmFyaWFudCAmIHNwYXRpYWwgaGllcmFyY2h5IGltcG9ydGFudAotIFdoYXQgdGhlIGdlbmVyYWwgc3RydWN0dXJlIG9mIENOTiBtb2RlbHMgbG9va3MgbGlrZQotIFdoYXQgaXMgdGhlIGNvbnZvbHV0aW9uIG9wZXJhdGlvbgotIFdoYXQgYXJlIGZlYXR1cmUgbWFwcwotIEhvdyBwb29saW5nIGhlbHBzIGJ5IGRvd25zYW1wbGluZwoKIyBSZXF1aXJlZCBwYWNrYWdlcwoKYGBge3J9CmxpYnJhcnkoa2VyYXMpCmBgYAoKIyBQcmVwYXJlIGRhdGEKCkxldCdzIGltcG9ydCBvdXIgdHJhaW5pbmcgYW5kIHRlc3QgZGF0YS4gUmF0aGVyIHRoYW4gdHVybiBvdXIgaW1hZ2UgZGF0YSBpbnRvIGEgCjJEIHRlbnNvciBhcyB3ZSBkaWQgaW4gdGhlIGVhcmxpZXIgbW9kdWxlLCBoZXJlIHdlIGNvbnZlcnQgb3VyIGRhdGEgdG8gYSA0RCAKdGVuc29yIHRoYXQgaGFzOgoKLSA2MEsgc2FtcGxlcyAodHJhaW4pIGFuZCAxMEsgc2FtcGxlcyAodGVzdCkKLSBoZWlnaHQgeCB3aWR0aCA9IDI4eDI4IHBpeGVscwotIDEgY29sb3IgY2hhbm5lbCAodGhlc2UgYXJlIGdyYXkgc2NhbGUgcmF0aGVyIHRoYW4gUkdCLCB3aGljaCBoYXMgMyBjb2xvcgogIGNoYW5uZWxzKQoKIVtdKGltYWdlcy80RF90ZW5zb3IucG5nKQoKQXMgYmVmb3JlLCBvdXIgcGl4ZWxzIHJhbmdlIGZyb20gMC0yNTUgc28gd2UgcmUtc2NhbGUgdGhlbSB0byBiZSBiZXR3ZWVuIDAtMS4KCmBgYHtyIGdldC1kYXRhfQptbmlzdCA8LSBkYXRhc2V0X21uaXN0KCkKYyhjKHRyYWluX2ltYWdlcywgdHJhaW5fbGFiZWxzKSwgYyh0ZXN0X2ltYWdlcywgdGVzdF9sYWJlbHMpKSAlPC0lIG1uaXN0Cgp0cmFpbl9pbWFnZXMgPC0gYXJyYXlfcmVzaGFwZSh0cmFpbl9pbWFnZXMsIGMoNjAwMDAsIDI4LCAyOCwgMSkpIC8gMjU1CnRlc3RfaW1hZ2VzIDwtIGFycmF5X3Jlc2hhcGUodGVzdF9pbWFnZXMsIGMoMTAwMDAsIDI4LCAyOCwgMSkpIC8gMjU1Cgp0cmFpbl9sYWJlbHMgPC0gdG9fY2F0ZWdvcmljYWwodHJhaW5fbGFiZWxzKQp0ZXN0X2xhYmVscyA8LSB0b19jYXRlZ29yaWNhbCh0ZXN0X2xhYmVscykKYGBgCgojIENOTjogRmVhdHVyZSBkZXRlY3RvcgoKVG8gcnVuIGEgQ05OIHdlIHdpbGwgZm9sbG93IGEgdmVyeSBzaW1pbGFyIGFwcHJvYWNoIHRvIHdoYXQgd2UndmUgc2VlbiBzbyBmYXIuIApUaGUgbWFpbiBkaWZmZXJlbmNlIGlzIHRoYXQgd2UgY3JlYXRlIGEgY29udm9sdXRpb24gYW5kIG1heCBwb29saW5nIHByb2NlZHVyZSAKcHJpb3IgdG8gb3VyIGRlbnNsZXkgY29ubmVjdGVkIE1MUC4gVGhpcyBpcyBrbm93biBhcyBvdXIgX2ZlYXR1cmUgZGV0ZWN0b3JfIHN0ZXAuIAoKIVtdKGltYWdlcy9DTk4tZmVhdC1leHRyYWN0LnBuZykKCldlJ2xsIGRpc2N1c3MgdGhlIGRldGFpbHMgb2YgdGhlc2Ugc3RlcHMgc2hvcnRseSBidXQgZm9yIG5vdyBqdXN0IHJlYWxpemUgdGhlc2UgCm1haW4gcG9pbnRzOgoKMS4gb3VyIGBpbnB1dF9zaGFwZWAgaXMgMjh4MjggaW1hZ2Ugd2l0aCAxIGNvbG9yIGNoYW5uZWwsCjIuIHRoZSBvdXRwdXQgb2YgZWFjaCBgbGF5ZXJfY29udjJkKClgIGFuZCBgbGF5ZXJfbWF4X3Bvb2xpbmdfMmQoKWAgaXMgYSAzRCAKICAgdGVuc29yIG9mIHNoYXBlIChoZWlnaHQsIHdpZHRoLCBjaGFubmVscyksCjMuIHRoZSBoZWlnaHQgYW5kIHdpZHRoIGRpbWVuc2lvbnMgdGVuZCB0byBzaHJpbmsgYXMgeW91IGdvIGRlZXBlciBpbiB0aGUgbmV0d29yaywKNC4gd2hpbGUgdGhlIG51bWJlciBvZiBjaGFubmVscyBpbmNyZWFzZS4KCmBgYHtyIGNyZWF0ZS1jbm59Cm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUKICAKICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSAzMiwga2VybmVsX3NpemUgPSBjKDMsIDMpLCBhY3RpdmF0aW9uID0gInJlbHUiLCAKICAgICAgICAgICAgICAgIGlucHV0X3NoYXBlID0gYygyOCwgMjgsIDEpKSAlPiUKICBsYXllcl9tYXhfcG9vbGluZ18yZChwb29sX3NpemUgPSBjKDIsIDIpKSAlPiUKICAKICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSA2NCwga2VybmVsX3NpemUgPSBjKDMsIDMpLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUKICBsYXllcl9tYXhfcG9vbGluZ18yZChwb29sX3NpemUgPSBjKDIsIDIpKSAlPiUKICAKICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSA2NCwga2VybmVsX3NpemUgPSBjKDMsIDMpLCBhY3RpdmF0aW9uID0gInJlbHUiKQoKc3VtbWFyeShtb2RlbCkKYGBgCgojIENOTjogQ2xhc3NpZmllcgoKTmV4dCwgd2UgZmVlZCB0aGUgbGFzdCBvdXRwdXQgdGVuc29yIG9mIHNoYXBlIGAoMywgMywgNjQpYCBpbnRvIGEgZGVuc2VseSAKY29ubmVjdGVkIE1MUC4gVGhpcyBNTFAgaXMgdG8gY2xhc3NpZnkgb3VyIGltYWdlcyBhbmQgd2Ugb2Z0ZW4gcmVmZXIgdG8gdGhpcyAKcGFydCBvZiBvdXIgQ05OIGFzIHRoZSBfY2xhc3NpZmllcl8uIFRoZSBvbmx5IG5ldyBjb25jZXB0IGhlcmUgaXMgYGxheWVyX2ZsYXR0ZW4oKWAgCndoaWNoIGlzIHJlZHVjaW5nIHRoZSAzRCB0ZW5zb3IgZm9yIGEgZ2l2ZW4gaW1hZ2UgdG8gYSAxRCB0ZW5zb3IuCgohW10oaW1hZ2VzL0NOTi1jbGFzc2lmaWVyLnBuZykKCmBgYHtyIGFkZC1jbGFzc2lmaWVyfQptb2RlbCAlPiUKICBsYXllcl9mbGF0dGVuKCkgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSA2NCwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxMCwgYWN0aXZhdGlvbiA9ICJzb2Z0bWF4IikKCnN1bW1hcnkobW9kZWwpCmBgYAoKIyBDTk46IENvbXBpbGUgJiB0cmFpbgoKVGhlc2Ugc3RlcHMgYXJlIHRoZSBzYW1lIGFzIGJlZm9yZS4gSG93ZXZlciwgeW91IHdpbGwgbm90aWNlIHRoYXQgdHJhaW5pbmcgdGFrZXMgCmxvbmdlciwgd2hpY2ggaXMgZHVlIHRvIHRoZSBhZGRlZCBDTk4gcHJvY2VkdXJlLiBXaGlsZSB0aGlzIG1vZGVsIGlzIHRyYWluaW5nLApsZXQncyBkaXNjdXNzIHdoYXQncyBoYXBwZW5pbmcgdW5kZXIgdGhlIGhvb2Qgb2YgYSBDTk4gW+KEue+4j10oaHR0cDovL2JpdC5seS9kbC0wMyMxMykuCgpBbHRob3VnaCB3ZSB1c2VkIGEgZmFpcmx5IGJhc2ljIG1vZGVsIHdpdGhvdXQgb3B0aW1pemluZyB0aGUgbGVhcm5pbmcgcmF0ZSwKbW9kZWwgY2FwYWNpdHksIGJhdGNoLCBldGMuLCB5b3Ugd2lsbCBhbHNvIG5vdGljZSB0aGF0IG91ciBtb2RlbCBwZXJmb3JtYW5jZSBpcwpzdXBlcmlvciB0byBvdXIgTUxQIG1vZGVsIGZyb20gdGhlIGVhcmxpZXIgbW9kdWxlOgoKLSBNTFA6IGxvc3MgKH4gMC4wNykgJiBhY2N1cmFjeSAofiAwLjk3NSkKLSBDTk46IGxvc3MgKH4gMC4wNCkgJiBhY2N1cmFjeSAofiAwLjk5KSAKCmBgYHtyIHRyYWluLW1vZGVsfQptb2RlbCAlPiUgY29tcGlsZSgKICBvcHRpbWl6ZXIgPSAicm1zcHJvcCIsCiAgbG9zcyA9ICJjYXRlZ29yaWNhbF9jcm9zc2VudHJvcHkiLAogIG1ldHJpY3MgPSBjKCJhY2N1cmFjeSIpCikKCmhpc3RvcnkgPC0gbW9kZWwgJT4lIGZpdCgKICB0cmFpbl9pbWFnZXMsIHRyYWluX2xhYmVscywKICBlcG9jaHMgPSA1LCAKICBiYXRjaF9zaXplID0gMTI4LAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIKKQpgYGAKCmBgYHtyfQpoaXN0b3J5CmBgYAoKYGBge3IgbGVhcm5pbmctY3VydmV9CnBsb3QoaGlzdG9yeSkKYGBgCgojIEV2YWx1YXRpb24KClVzaW5nIG91ciBDTk4gd2Ugb2J0YWluIGEgdGVzdCBzZXQgYWNjdXJhY3kgb2YgfiAwLjk5LgoKYGBge3IgdGVzdC1ldmFsfQptb2RlbCAlPiUgZXZhbHVhdGUodGVzdF9pbWFnZXMsIHRlc3RfbGFiZWxzLCB2ZXJib3NlID0gRkFMU0UpCmBgYAoKIyBZb3VyIFR1cm4hICgxMCBtaW4pCgpTcGVuZCAxMCBtaW51dGVzIGFkanVzdGluZyB2YXJpb3VzIENOTiBjb21wb25lbnRzOgoKLSBDaGFuZ2UgdGhlIG51bWJlciBvZiBmaWx0ZXJzCi0gQ2hhbmdlIGZpbHRlci9rZXJuZWwgc2l6ZQotIEFkanVzdCB0aGUgc3RyaWRlCi0gQWRkIHBhZGRpbmcKLSBBZGQgbW9yZSBjb252b2x1dGlvbiBsYXllcnMKCk9yIGtlZXAgdGhlIHNhbWUgQ05OIGNvbXBvbmVudHMgYXMgYWJvdmUgYnV0IGFwcGx5IHNvbWUgb2YgdGhlIHR1bmluZyBzdGVwcyB3ZQpjb3ZlcmVkIHRoaXMgbW9ybmluZzoKCi0gVHJ5IGRpZmZlcmVudCBhZGFwdGl2ZSBsZWFybmluZyByYXRlIG9wdGltaXplcnMgYW5kIGxlYXJuaW5nIHJhdGUgdmFsdWVzCi0gSG93IGRvZXMgYmF0Y2ggc2l6ZSBpbXBhY3QgcGVyZm9ybWFuY2UKLSBZb3UgY2FuIGV2ZW4gdHJ5IHRvIGFkZCB3ZWlnaHQgZGVjYXkgb3IgZHJvcG91dCB0byBlYWNoIGxheWVyIHRvIGNvbnRyb2wKICBvdmVyZml0dGluZzoKICAgIC0gd2VpZ2h0IGRlY2F5cyBjYW4gYmUgYXBwbGllZCB3aXRoIGBrZXJuZWxfcmVndWxhcml6ZXJgIHdpdGhpbiBgbGF5ZXJfY29udl8yZGAKICAgIC0gYGxheWVyX2Ryb3BvdXQoKWAgY2FuIGJlIGFwcGxpZWQgYmVmb3JlIG9yIGFmdGVyIHBvb2xpbmcgYnV0IGlzIG1vcmUgY29tbW9ubHkKICAgICAgIHNlZW4gYWZ0ZXIuIE5vdGUgdGhhdCBkcm9wb3V0IGluIENOTnMgd2lsbCByYW5kb21seSBkcm9wIG91dCBlbnRpcmUKICAgICAgIGZlYXR1cmUgbWFwcwoKYGBge3IgeW91ci10dXJuLWRlZmluZX0KbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JQogIAogIGxheWVyX2NvbnZfMmQoZmlsdGVycyA9IF9fX18sIGtlcm5lbF9zaXplID0gX19fXywgYWN0aXZhdGlvbiA9ICJyZWx1IiwgCiAgICAgICAgICAgICAgICBpbnB1dF9zaGFwZSA9IGMoMjgsIDI4LCAxKSkgJT4lCiAgbGF5ZXJfbWF4X3Bvb2xpbmdfMmQocG9vbF9zaXplID0gX19fXykgJT4lCiAgCiAgbGF5ZXJfY29udl8yZChmaWx0ZXJzID0gX19fXywga2VybmVsX3NpemUgPSBfX19fLCBhY3RpdmF0aW9uID0gInJlbHUiKSAlPiUKICBsYXllcl9tYXhfcG9vbGluZ18yZChwb29sX3NpemUgPSBfX19fKSAlPiUKICAKICBsYXllcl9jb252XzJkKGZpbHRlcnMgPSBfX19fLCBrZXJuZWxfc2l6ZSA9IF9fX18sIGFjdGl2YXRpb24gPSAicmVsdSIpCgptb2RlbCAlPiUKICBfX19fX19fICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gNjQsIGFjdGl2YXRpb24gPSAicmVsdSIpICU+JQogIGxheWVyX2RlbnNlKHVuaXRzID0gMTAsIGFjdGl2YXRpb24gPSAic29mdG1heCIpCgptb2RlbCAlPiUgY29tcGlsZSgKICBvcHRpbWl6ZXIgPSAicm1zcHJvcCIsCiAgbG9zcyA9ICJjYXRlZ29yaWNhbF9jcm9zc2VudHJvcHkiLAogIG1ldHJpY3MgPSBjKCJhY2N1cmFjeSIpCikKCnN1bW1hcnkobW9kZWwpCmBgYAoKYGBge3IgeW91ci10dXJuLXRyYWlufQpoaXN0b3J5IDwtIG1vZGVsICU+JSBmaXQoCiAgdHJhaW5faW1hZ2VzLCB0cmFpbl9sYWJlbHMsCiAgZXBvY2hzID0gNSwgCiAgYmF0Y2hfc2l6ZSA9IDY0LAogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjIKKQpgYGAKCiMgS2V5IHRha2Vhd2F5cwoKKiBDTk5zIGFsbG93IHVzIHRvIGNhcHR1cmUgYW5kIGNvbnRyb2wgZm9yIGltYWdlIHZhcmlhbmNlCgoqIFRoZSBjb252b2x1dGlvbiBsYXllciBwcm92aWRlcyB0aGUgbWFpbiBtZWNoYW5pc20gZm9yIGZlYXR1cmUgZW5naW5lZXJpbmcKICAgLSBXZSBzbGlkZSBhIGZpbHRlci9rZXJuZWwgb3ZlciBvdXIgaW1hZ2VzIHRvIGNyZWF0ZSBmZWF0dXJlIG1hcHMKICAgLSBXZSBjYW4gdXNlIHN0cmlkaW5nIGFuZCBwYWRkaW5nIHRvIGNvbnRyb2wgdGhlIHNpemUgb2Ygb3VyIGZlYXR1cmUgbWFwcwogICAtIEFwcGx5IHBvb2xpbmcgdG8gZG93bnNhbXBsZQogICAKKiBBbHdheXMgZmxhdHRlbiB0aGUgb3V0cHV0IG9mIHRoZSBjb252b2x1dGlvbiBsYXllciB0byBmZWVkIGludG8gYSBkZW5zZSBsYXllcgoKKiBTaW5jZSBhbGwgb3VyIGZlYXR1cmUgZW5naW5lZXJpbmcgb2NjdXJzIGluIHRoZSBjb252b2x1dGlvbiBsYXllciwgd2UgdHlwaWNhbGx5CiAgbmVlZCBvbmx5IG9uZSBoaWRkZW4gZGVuc2UgbGF5ZXIgd2l0aCBmYXIgZmV3ZXIgdW5pdHMgYW5kIGVwb2NocyB0byB0cmFpbiBvdXIKICBtb2RlbAogIApb8J+PoF0oaHR0cHM6Ly9naXRodWIuY29tL3JzdHVkaW8tY29uZi0yMDIwL2RsLWtlcmFzLXRmKQ==