library(tidyverse)
library(magrittr)
library(keras)

Using large pretrained models as your foundation

  • This notebook is based on https://www.learnopencv.com/keras-tutorial-fine-tuning-using-pre-trained-models/
  • Training large CNNs is costly (compute, time) and often we don’t have enough data to bring these models to reasonable performance levels. This is where transfer-learning comes in.
  • Assuming that the early layers of a CNN learn very basic features (edges, lines etc.) we can consider a strategy, where we take a large pretrained model, discard the last couple of layers (where we would assume the more abstract information) and train it with our new data.
  • This is how you, for instance, could fine-tune a CNN model to some very specific medical imaging data where you simply cannot have so many examples.

Load the data

It’s the same cats&dog data…

# Let's start by downloading and exploring the data
temp <- tempfile()
download.file('https://storage.googleapis.com/sds-file-transfer/dataset.zip',temp)
trying URL 'https://storage.googleapis.com/sds-file-transfer/dataset.zip'
Content type 'application/zip' length 228470636 bytes (217.9 MB)
==================================================
downloaded 217.9 MB
unzip(temp)
unlink(temp)

Preprocessing

Same as in the former notebook

library(tidyverse)
library(magrittr)

library(keras)
# how much data?
train_size <- c(list.files(path = 'dataset/training_set/cats'), list.files(path = 'dataset/training_set/dogs')) %>% length()
test_size <- c(list.files(path = 'dataset/test_set/cats'), list.files(path = 'dataset/test_set/dogs')) %>% length()
train_data_gen = image_data_generator(rescale = 1/255, shear_range = 0.2, zoom_range = 0.2, horizontal_flip = TRUE)
test_data_gen <- image_data_generator(rescale = 1/255)  
train_image_array_gen <- flow_images_from_directory(train_files_path, train_data_gen, target_size = target_size, class_mode = "binary", 
                                                    classes = class_list,batch_size = 32, seed = 1337)
Found 8000 images belonging to 2 classes.
test_image_array_gen <- flow_images_from_directory(test_files_path, test_data_gen, target_size = target_size, class_mode = "binary",
                                          classes = class_list, batch_size = 32, seed = 1337)
Found 2000 images belonging to 2 classes.

Loading pretrained model

Keras has some great built-in applications - basically pretrained models with some nice functional overhead. We will start by loadeing VGG16 (a rather large model with many many many layers that has been trained on imagenet)

# create the base pre-trained model
base_model <- application_vgg16(
  include_top = FALSE,
  weights='imagenet',
  input_shape = c(img_height, img_width, channels))
Downloading data from https://storage.googleapis.com/tensorflow/keras-applications/vgg16/vgg16_weights_tf_dim_ordering_tf_kernels_notop.h5

    8192/58889256 [..............................] - ETA: 1s
  106496/58889256 [..............................] - ETA: 28s
  475136/58889256 [..............................] - ETA: 12s
 1425408/58889256 [..............................] - ETA: 6s 
 3178496/58889256 [>.............................] - ETA: 3s
 3817472/58889256 [>.............................] - ETA: 4s
 4898816/58889256 [=>............................] - ETA: 3s
 6299648/58889256 [==>...........................] - ETA: 5s
 8167424/58889256 [===>..........................] - ETA: 4s
 8396800/58889256 [===>..........................] - ETA: 4s
 9707520/58889256 [===>..........................] - ETA: 4s
10428416/58889256 [====>.........................] - ETA: 4s
13721600/58889256 [=====>........................] - ETA: 3s
14147584/58889256 [======>.......................] - ETA: 3s
16785408/58889256 [=======>......................] - ETA: 3s
18046976/58889256 [========>.....................] - ETA: 3s
20930560/58889256 [=========>....................] - ETA: 2s
22208512/58889256 [==========>...................] - ETA: 2s
25174016/58889256 [===========>..................] - ETA: 3s
26370048/58889256 [============>.................] - ETA: 3s
29188096/58889256 [=============>................] - ETA: 2s
30564352/58889256 [==============>...............] - ETA: 2s
33947648/58889256 [================>.............] - ETA: 1s
34004992/58889256 [================>.............] - ETA: 2s
36298752/58889256 [=================>............] - ETA: 1s
37543936/58889256 [==================>...........] - ETA: 1s
40148992/58889256 [===================>..........] - ETA: 1s
42115072/58889256 [====================>.........] - ETA: 1s
43048960/58889256 [====================>.........] - ETA: 1s
46161920/58889256 [======================>.......] - ETA: 0s
46833664/58889256 [======================>.......] - ETA: 0s
49881088/58889256 [========================>.....] - ETA: 0s
50651136/58889256 [========================>.....] - ETA: 0s
53993472/58889256 [==========================>...] - ETA: 0s
56467456/58889256 [===========================>..] - ETA: 0s
57712640/58889256 [============================>.] - ETA: 0s
58892288/58889256 [==============================] - 3s 0us/step
  • Now we got the model, where we can add some own layers
predictions <- base_model$output %>% 
  layer_global_average_pooling_2d() %>% 
  layer_dense(units = 1024, activation = 'relu') %>% 
  layer_dense(units = 1, activation = 'sigmoid')
# this is the model we will train
model <- keras_model(inputs = base_model$input, outputs = predictions)

The trick is, to “freeze” most of the initial layers and thereby preserve the majority of the information embedded in the model already. We will only train the added new dense layer as well as an output layer that fit’s our purpose with the model - finding cats and dogs

# first: train only the top layers 
for (layer in base_model$layers)
  layer$trainable <- FALSE
# compile the model (should be done *after* setting layers to non-trainable)
model %>% compile(optimizer = 'adam', 
                  loss = 'binary_crossentropy',
                  metric = 'accuracy')
# train the model on the new data for a few epochs
model %>% fit_generator(
  train_image_array_gen,
  steps_per_epoch = 800,
  epochs = 2, 
  validation_data = test_image_array_gen,
  validation_steps = 100,
  verbose = FALSE,
  )
  • at this point, the top layers are well trained and we can start fine-tuning convolutional layers.
  • We will freeze the bottom N layers nd train the remaining top layers.
# let's visualize layer names and layer indices to see how many layers we should freeze:
layers <- base_model$layers
for (i in 1:length(layers))
  cat(i, layers[[i]]$name, "\n")
1 input_1 
2 block1_conv1 
3 block1_conv2 
4 block1_pool 
5 block2_conv1 
6 block2_conv2 
7 block2_pool 
8 block3_conv1 
9 block3_conv2 
10 block3_conv3 
11 block3_pool 
12 block4_conv1 
13 block4_conv2 
14 block4_conv3 
15 block4_pool 
16 block5_conv1 
17 block5_conv2 
18 block5_conv3 
19 block5_pool 
# we chose to train the top 2 inblocks, i.e. we will freeze the first layers and unfreeze the rest:
for (i in 1:18)
  layers[[i]]$trainable <- FALSE
for (i in 19:length(layers))
  layers[[i]]$trainable <- TRUE
# we need to recompile the model for these modifications to take effect
model %>% compile(optimizer = 'adam', 
                  loss = 'binary_crossentropy',
                  metric = 'accuracy')
# we train our model again (this time fine-tuning the top 2  blocks
# alongside the top Dense layers
# train the model on the new data for a few epochs
model %>% fit_generator(
  train_image_array_gen,
  steps_per_epoch = 800,
  epochs = 2, 
  verbose = FALSE,
  )
hist %>% plot()

Evaluation returns loss and accuracy

model %>% evaluate_generator(test_image_array_gen, steps=500)
     loss  accuracy 
0.4224312 0.8051285 

Ok, now we can delete all the images again…

# unlink("dataset", recursive = TRUE)
LS0tCnRpdGxlOiAnTmV1cmFsIE5ldHdvcmtzIEFwcGxpY2F0aW9uOiBUcmFuc2ZlciBsZWFybmluZyB3aXRoIENOTnMgKFIpJwphdXRob3I6ICJEYW5pZWwgUy4gSGFpbiAoZHNoQGJ1c2luZXNzLmFhdS5kaykiCmRhdGU6ICJVcGRhdGVkIGByIGZvcm1hdChTeXMudGltZSgpLCAnJUIgJWQsICVZJylgIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazoKICAgIGNvZGVfZm9sZGluZzogc2hvdwogICAgZGZfcHJpbnQ6IHBhZ2VkCiAgICB0b2M6IHRydWUKICAgIHRvY19kZXB0aDogMgogICAgdG9jX2Zsb2F0OgogICAgICBjb2xsYXBzZWQ6IGZhbHNlCiAgICB0aGVtZTogZmxhdGx5Ci0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CiMjIyBHZW5lcmljIHByZWFtYmxlCnJtKGxpc3Q9bHMoKSkKU3lzLnNldGVudihMQU5HID0gImVuIikgIyBGb3IgZW5nbGlzaCBsYW5ndWFnZQpvcHRpb25zKHNjaXBlbiA9IDUpICMgVG8gZGVhY3RpdmF0ZSBhbm5veWluZyBzY2llbnRpZmljIG51bWJlciBub3RhdGlvbgoKIyMjIEtuaXRyIG9wdGlvbnMKbGlicmFyeShrbml0cikgIyBGb3IgZGlzcGxheSBvZiB0aGUgbWFya2Rvd24Ka25pdHI6Om9wdHNfY2h1bmskc2V0KHdhcm5pbmc9RkFMU0UsCiAgICAgICAgICAgICAgICAgICAgIG1lc3NhZ2U9RkFMU0UsCiAgICAgICAgICAgICAgICAgICAgIGNvbW1lbnQ9RkFMU0UsIAogICAgICAgICAgICAgICAgICAgICBmaWcuYWxpZ249ImNlbnRlciIKICAgICAgICAgICAgICAgICAgICAgKQpgYGAKCgpgYGB7cn0KbGlicmFyeSh0aWR5dmVyc2UpCmxpYnJhcnkobWFncml0dHIpCmxpYnJhcnkoa2VyYXMpCmBgYAoKIyBVc2luZyBsYXJnZSBwcmV0cmFpbmVkIG1vZGVscyBhcyB5b3VyIGZvdW5kYXRpb24KCiogVGhpcyBub3RlYm9vayBpcyBiYXNlZCBvbiBodHRwczovL3d3dy5sZWFybm9wZW5jdi5jb20va2VyYXMtdHV0b3JpYWwtZmluZS10dW5pbmctdXNpbmctcHJlLXRyYWluZWQtbW9kZWxzLwoqIFRyYWluaW5nIGxhcmdlIENOTnMgaXMgY29zdGx5IChjb21wdXRlLCB0aW1lKSBhbmQgb2Z0ZW4gd2UgZG9uJ3QgaGF2ZSBlbm91Z2ggZGF0YSB0byBicmluZyB0aGVzZSBtb2RlbHMgdG8gcmVhc29uYWJsZSBwZXJmb3JtYW5jZSBsZXZlbHMuIFRoaXMgaXMgd2hlcmUgdHJhbnNmZXItbGVhcm5pbmcgY29tZXMgaW4uCiogQXNzdW1pbmcgdGhhdCB0aGUgZWFybHkgbGF5ZXJzIG9mIGEgQ05OIGxlYXJuIHZlcnkgYmFzaWMgZmVhdHVyZXMgKGVkZ2VzLCBsaW5lcyBldGMuKSB3ZSBjYW4gY29uc2lkZXIgYSBzdHJhdGVneSwgd2hlcmUgd2UgdGFrZSBhIGxhcmdlIHByZXRyYWluZWQgbW9kZWwsIGRpc2NhcmQgdGhlIGxhc3QgY291cGxlIG9mIGxheWVycyAod2hlcmUgd2Ugd291bGQgYXNzdW1lIHRoZSBtb3JlIGFic3RyYWN0IGluZm9ybWF0aW9uKSBhbmQgdHJhaW4gaXQgd2l0aCBvdXIgbmV3IGRhdGEuCiogVGhpcyBpcyBob3cgeW91LCBmb3IgaW5zdGFuY2UsIGNvdWxkIGZpbmUtdHVuZSBhIENOTiBtb2RlbCB0byBzb21lIHZlcnkgc3BlY2lmaWMgbWVkaWNhbCBpbWFnaW5nIGRhdGEgd2hlcmUgeW91IHNpbXBseSBjYW5ub3QgaGF2ZSBzbyBtYW55IGV4YW1wbGVzLgoKIyBMb2FkIHRoZSBkYXRhCgpJdCdzIHRoZSBzYW1lIGNhdHMmZG9nIGRhdGEuLi4KCmBgYHtyfQojIExldCdzIHN0YXJ0IGJ5IGRvd25sb2FkaW5nIGFuZCBleHBsb3JpbmcgdGhlIGRhdGEKdGVtcCA8LSB0ZW1wZmlsZSgpCmRvd25sb2FkLmZpbGUoJ2h0dHBzOi8vc3RvcmFnZS5nb29nbGVhcGlzLmNvbS9zZHMtZmlsZS10cmFuc2Zlci9kYXRhc2V0LnppcCcsdGVtcCkKdW56aXAodGVtcCkKdW5saW5rKHRlbXApCmBgYAoKIyBQcmVwcm9jZXNzaW5nCgpTYW1lIGFzIGluIHRoZSBmb3JtZXIgbm90ZWJvb2sKCmBgYHtyfQpjbGFzc19saXN0IDwtIGMoJ2NhdHMnLCAnZG9ncycpCm91dHB1dF9uIDwtIGxlbmd0aChjbGFzc19saXN0KQppbWdfd2lkdGggPC0gNjQKaW1nX2hlaWdodCA8LSA2NAp0YXJnZXRfc2l6ZSA8LSBjKGltZ193aWR0aCwgaW1nX2hlaWdodCkKY2hhbm5lbHMgPC0gMwp0cmFpbl9maWxlc19wYXRoIDwtICdkYXRhc2V0L3RyYWluaW5nX3NldCcKdGVzdF9maWxlc19wYXRoIDwtICdkYXRhc2V0L3Rlc3Rfc2V0JwpiYXRjaF9zaXplIDwtIDMyCmBgYAoKYGBge3J9CiMgaG93IG11Y2ggZGF0YT8KdHJhaW5fc2l6ZSA8LSBjKGxpc3QuZmlsZXMocGF0aCA9ICdkYXRhc2V0L3RyYWluaW5nX3NldC9jYXRzJyksIGxpc3QuZmlsZXMocGF0aCA9ICdkYXRhc2V0L3RyYWluaW5nX3NldC9kb2dzJykpICU+JSBsZW5ndGgoKQp0ZXN0X3NpemUgPC0gYyhsaXN0LmZpbGVzKHBhdGggPSAnZGF0YXNldC90ZXN0X3NldC9jYXRzJyksIGxpc3QuZmlsZXMocGF0aCA9ICdkYXRhc2V0L3Rlc3Rfc2V0L2RvZ3MnKSkgJT4lIGxlbmd0aCgpCmBgYAoKCmBgYHtyfQp0cmFpbl9kYXRhX2dlbiA9IGltYWdlX2RhdGFfZ2VuZXJhdG9yKHJlc2NhbGUgPSAxLzI1NSwgc2hlYXJfcmFuZ2UgPSAwLjIsIHpvb21fcmFuZ2UgPSAwLjIsIGhvcml6b250YWxfZmxpcCA9IFRSVUUpCnRlc3RfZGF0YV9nZW4gPC0gaW1hZ2VfZGF0YV9nZW5lcmF0b3IocmVzY2FsZSA9IDEvMjU1KSAgCmBgYAoKCmBgYHtyfQp0cmFpbl9pbWFnZV9hcnJheV9nZW4gPC0gZmxvd19pbWFnZXNfZnJvbV9kaXJlY3RvcnkoCiAgdHJhaW5fZmlsZXNfcGF0aCwgCiAgdHJhaW5fZGF0YV9nZW4sIAogIHRhcmdldF9zaXplID0gdGFyZ2V0X3NpemUsIAogIGNsYXNzX21vZGUgPSAiYmluYXJ5IiwgIAogIGNsYXNzZXMgPSBjbGFzc19saXN0LAogIGJhdGNoX3NpemUgPSBiYXRjaF9zaXplLCAKICBzZWVkID0gMTMzNykKCnRlc3RfaW1hZ2VfYXJyYXlfZ2VuIDwtIGZsb3dfaW1hZ2VzX2Zyb21fZGlyZWN0b3J5KHRlc3RfZmlsZXNfcGF0aCwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHRlc3RfZGF0YV9nZW4sIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0YXJnZXRfc2l6ZSA9IHRhcmdldF9zaXplLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NfbW9kZSA9ICJiaW5hcnkiLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NlcyA9IGNsYXNzX2xpc3QsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBiYXRjaF9zaXplID0gYmF0Y2hfc2l6ZSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlZWQgPSAxMzM3KQpgYGAKCiMgTG9hZGluZyBwcmV0cmFpbmVkIG1vZGVsCgpLZXJhcyBoYXMgc29tZSBncmVhdCBidWlsdC1pbiBhcHBsaWNhdGlvbnMgLSBiYXNpY2FsbHkgcHJldHJhaW5lZCBtb2RlbHMgd2l0aCBzb21lIG5pY2UgZnVuY3Rpb25hbCBvdmVyaGVhZC4gV2Ugd2lsbCBzdGFydCBieSBsb2FkZWluZyBWR0cxNiAoYSByYXRoZXIgbGFyZ2UgbW9kZWwgd2l0aCBtYW55IG1hbnkgbWFueSBsYXllcnMgdGhhdCBoYXMgYmVlbiB0cmFpbmVkIG9uIGltYWdlbmV0KQoKCmBgYHtyfQojIGNyZWF0ZSB0aGUgYmFzZSBwcmUtdHJhaW5lZCBtb2RlbApiYXNlX21vZGVsIDwtIGFwcGxpY2F0aW9uX3ZnZzE2KAogIGluY2x1ZGVfdG9wID0gRkFMU0UsCiAgd2VpZ2h0cz0naW1hZ2VuZXQnLAogIGlucHV0X3NoYXBlID0gYyhpbWdfaGVpZ2h0LCBpbWdfd2lkdGgsIGNoYW5uZWxzKSkKYGBgCgoqIE5vdyB3ZSBnb3QgdGhlIG1vZGVsLCB3aGVyZSB3ZSBjYW4gYWRkIHNvbWUgb3duIGxheWVycwoKCgpgYGB7cn0KcHJlZGljdGlvbnMgPC0gYmFzZV9tb2RlbCRvdXRwdXQgJT4lIAogIGxheWVyX2ZsYXR0ZW4odHJhaW5hYmxlID0gVCkgJT4lCiAgbGF5ZXJfZGVuc2UoMTAyNCwgYWN0aXZhdGlvbiA9ICdyZWx1JywgdHJhaW5hYmxlID0gVCkgJT4lCiAgbGF5ZXJfZHJvcG91dCgwLjQsIHRyYWluYWJsZSA9IFQpICU+JQogIGxheWVyX2RlbnNlKDg0LCBhY3RpdmF0aW9uID0gJ3JlbHUnLCB0cmFpbmFibGU9VCkgJT4lICAKICBsYXllcl9kZW5zZSh1bml0cyA9IDEsIGFjdGl2YXRpb24gPSAnc2lnbW9pZCcsIHRyYWluYWJsZT1UKQpgYGAKCgpgYGB7cn0KIyB0aGlzIGlzIHRoZSBtb2RlbCB3ZSB3aWxsIHRyYWluCm1vZGVsIDwtIGtlcmFzX21vZGVsKGlucHV0cyA9IGJhc2VfbW9kZWwkaW5wdXQsIG91dHB1dHMgPSBwcmVkaWN0aW9ucykKYGBgCgoqVGhlIHRyaWNrIGlzLCB0byAiZnJlZXplIiBtb3N0IG9mIHRoZSBpbml0aWFsIGxheWVycyBhbmQgdGhlcmVieSBwcmVzZXJ2ZSB0aGUgbWFqb3JpdHkgb2YgdGhlIGluZm9ybWF0aW9uIGVtYmVkZGVkIGluIHRoZSBtb2RlbCBhbHJlYWR5LiAKKiBXZSB3aWxsIG9ubHkgdHJhaW4gdGhlIGFkZGVkIG5ldyBkZW5zZSBsYXllciBhcyB3ZWxsIGFzIGFuIG91dHB1dCBsYXllciB0aGF0IGZpdCdzIG91ciBwdXJwb3NlIHdpdGggdGhlIG1vZGVsIC0gZmluZGluZyBjYXRzIGFuZCBkb2dzCgpgYGB7cn0KIyBmaXJzdDogdHJhaW4gb25seSB0aGUgdG9wIGxheWVycyAKZm9yIChsYXllciBpbiBiYXNlX21vZGVsJGxheWVycykKICBsYXllciR0cmFpbmFibGUgPC0gRkFMU0UKYGBgCgpgYGB7cn0KIyBjb21waWxlIHRoZSBtb2RlbCAoc2hvdWxkIGJlIGRvbmUgKmFmdGVyKiBzZXR0aW5nIGxheWVycyB0byBub24tdHJhaW5hYmxlKQptb2RlbCAlPiUgY29tcGlsZShvcHRpbWl6ZXIgPSAnYWRhbScsIAogICAgICAgICAgICAgICAgICBsb3NzID0gJ2JpbmFyeV9jcm9zc2VudHJvcHknLAogICAgICAgICAgICAgICAgICBtZXRyaWMgPSAnYWNjdXJhY3knKQpgYGAKCiAKYGBge3J9CiMgdHJhaW4gdGhlIG1vZGVsIG9uIHRoZSBuZXcgZGF0YSBmb3IgYSBmZXcgZXBvY2hzCm1vZGVsICU+JSBmaXRfZ2VuZXJhdG9yKAogIHRyYWluX2ltYWdlX2FycmF5X2dlbiwKICBzdGVwc19wZXJfZXBvY2ggPSBhcy5pbnRlZ2VyKCh0cmFpbl9zaXplIC8gYmF0Y2hfc2l6ZSkgKSwKICBlcG9jaHMgPSAyLCAKICB2ZXJib3NlID0gRkFMU0UsCiAgKQpgYGAKCiogYXQgdGhpcyBwb2ludCwgdGhlIHRvcCBsYXllcnMgYXJlIHdlbGwgdHJhaW5lZCBhbmQgd2UgY2FuIHN0YXJ0IGZpbmUtdHVuaW5nIGNvbnZvbHV0aW9uYWwgbGF5ZXJzLiAKKiBXZSB3aWxsIGZyZWV6ZSB0aGUgYm90dG9tIE4gbGF5ZXJzIG5kIHRyYWluIHRoZSByZW1haW5pbmcgdG9wIGxheWVycy4gCgpgYGB7cn0KIyBsZXQncyB2aXN1YWxpemUgbGF5ZXIgbmFtZXMgYW5kIGxheWVyIGluZGljZXMgdG8gc2VlIGhvdyBtYW55IGxheWVycyB3ZSBzaG91bGQgZnJlZXplOgpsYXllcnMgPC0gYmFzZV9tb2RlbCRsYXllcnMKZm9yIChpIGluIDE6bGVuZ3RoKGxheWVycykpCiAgY2F0KGksIGxheWVyc1tbaV1dJG5hbWUsICJcbiIpCmBgYAoKYGBge3J9CiMgd2UgY2hvc2UgdG8gdHJhaW4gdGhlIHRvcCAyIGluYmxvY2tzLCBpLmUuIHdlIHdpbGwgZnJlZXplIHRoZSBmaXJzdCBsYXllcnMgYW5kIHVuZnJlZXplIHRoZSByZXN0Ogpmb3IgKGkgaW4gMToxNykKICBsYXllcnNbW2ldXSR0cmFpbmFibGUgPC0gRkFMU0UKZm9yIChpIGluIDE4Omxlbmd0aChsYXllcnMpKQogIGxheWVyc1tbaV1dJHRyYWluYWJsZSA8LSBUUlVFCmBgYAogCmBgYHtyfQojIHdlIG5lZWQgdG8gcmVjb21waWxlIHRoZSBtb2RlbCBmb3IgdGhlc2UgbW9kaWZpY2F0aW9ucyB0byB0YWtlIGVmZmVjdAptb2RlbCAlPiUgY29tcGlsZShvcHRpbWl6ZXIgPSAnYWRhbScsIAogICAgICAgICAgICAgICAgICBsb3NzID0gJ2JpbmFyeV9jcm9zc2VudHJvcHknLAogICAgICAgICAgICAgICAgICBtZXRyaWMgPSAnYWNjdXJhY3knKQpgYGAKCmBgYHtyfQojIHdlIHRyYWluIG91ciBtb2RlbCBhZ2FpbiAodGhpcyB0aW1lIGZpbmUtdHVuaW5nIHRoZSB0b3AgMiAgYmxvY2tzCiMgYWxvbmdzaWRlIHRoZSB0b3AgRGVuc2UgbGF5ZXJzCiMgdHJhaW4gdGhlIG1vZGVsIG9uIHRoZSBuZXcgZGF0YSBmb3IgYSBmZXcgZXBvY2hzCmhpc3QgPC0gbW9kZWwgJT4lIGZpdF9nZW5lcmF0b3IoCiAgdHJhaW5faW1hZ2VfYXJyYXlfZ2VuLAogIHN0ZXBzX3Blcl9lcG9jaCA9IGFzLmludGVnZXIoKHRyYWluX3NpemUgLyBiYXRjaF9zaXplKSApLAogIGVwb2NocyA9IDUsIAogIHZlcmJvc2UgPSBGQUxTRSwKICB2YWxpZGF0aW9uX2RhdGEgPSB0ZXN0X2ltYWdlX2FycmF5X2dlbiwKICB2YWxpZGF0aW9uX3N0ZXBzID0gYXMuaW50ZWdlcigodGVzdF9zaXplIC8gYmF0Y2hfc2l6ZSkgKSwKICB2ZXJib3NlID0gRkFMU0UKICApCmBgYAoKYGBge3J9Cmhpc3QgJT4lIHBsb3QoKQpgYGAKCgojIEV2YWx1YXRpb24gcmV0dXJucyBsb3NzIGFuZCBhY2N1cmFjeQoKYGBge3J9Cm1vZGVsICU+JSBldmFsdWF0ZV9nZW5lcmF0b3IodGVzdF9pbWFnZV9hcnJheV9nZW4sIHN0ZXBzPTUwMCkKYGBgCgpPaywgbm93IHdlIGNhbiBkZWxldGUgYWxsIHRoZSBpbWFnZXMgYWdhaW4uLi4KCmBgYHtyfQojIHVubGluaygiZGF0YXNldCIsIHJlY3Vyc2l2ZSA9IFRSVUUpCmBgYAoKIAoKIAoKIAoK