Warstwa rekurencyjna w pakiecie Keras

Proces, który zaimplementowaliśmy przy użyciu biblioteki Numpy, jest odpowiednikiem prostej warstwy o nazwie layer_simple_rnn pakietu Keras:

layer_simple_rnn(units = 32)

Pomiędzy tymi implementacjami jest jednak pewna mała różnica: warstwa layer_simple_rnn dzieli sekwencje na wsady (operację taką wykonują wszystkie warstwy Keras) — nie przetwarza pojedynczych sekwencji tak, jak to miało miejsce w przykładzie zaimplementowanym za pomocą prostego kodu R. W związku z tym warstwa ta przyjmuje obiekty wejściowe o kształcie (rozmiar_wsadu, kroki_czasu, cechy_wejściowe), a nie (kroki_czasu, cechy_wejściowe).

Warstwa layer_simple_rnn, podobnie jak wszystkie rekurencyjne warstwy pakietu Keras, może być uruchomiona w dwóch trybach: może zwracać pełne sekwencje kolejnych obiektów wyjściowych dla każdego kroku czasu (trójwymiarowe tensory o kształcie (rozmiar_wsadu, kroki_czasu, cechy_wyjściowe)) lub tylko ostatnie obiekty wyjściowe poszczególnych sekwencji wejściowych (dwuwymiarowe tensory o kształcie (rozmiar_wsadu, cechy_wyjściowe)). Wybór trybu pracy jest dokonywany za pomocą argumentu return_sequences. Przeanalizujmy przykład, w którym zastosowano warstwę SimpleRNN, a dane wyjściowe są zwracane tylko podczas przetwarzania ostatniego kroku czasu:

rr library(keras) model <- keras_model_sequential() %>% layer_embedding(input_dim = 10000, output_dim = 32) %>% layer_simple_rnn(units = 32) summary(model)

_______________________________________________________________________________________________________________
Layer (type)                                     Output Shape                                 Param #          
===============================================================================================================
embedding_6 (Embedding)                          (None, None, 32)                             320000           
_______________________________________________________________________________________________________________
simple_rnn_8 (SimpleRNN)                         (None, 32)                                   2080             
===============================================================================================================
Total params: 322,080
Trainable params: 322,080
Non-trainable params: 0
_______________________________________________________________________________________________________________

rr model <- keras_model_sequential() %>% layer_embedding(input_dim = 10000, output_dim = 32) %>% layer_simple_rnn(units = 32, return_sequences = TRUE) summary(model)

_______________________________________________________________________________________________________________
Layer (type)                                     Output Shape                                 Param #          
===============================================================================================================
embedding_7 (Embedding)                          (None, None, 32)                             320000           
_______________________________________________________________________________________________________________
simple_rnn_9 (SimpleRNN)                         (None, None, 32)                             2080             
===============================================================================================================
Total params: 322,080
Trainable params: 322,080
Non-trainable params: 0
_______________________________________________________________________________________________________________

Czasami warto utworzyć stos składający się z kilku warstw rekurencyjnych. Zwiększa to siłę tworzenia reprezentacji przez sieć. Przy takiej konfiguracji wszystkie warstwy pośrednie muszą zwracać pełną sekwencję obiektów wyjściowych:

rr model <- keras_model_sequential() %>% layer_embedding(input_dim = 10000, output_dim = 32) %>% layer_simple_rnn(units = 32, return_sequences = TRUE) %>% layer_simple_rnn(units = 32, return_sequences = TRUE) %>% layer_simple_rnn(units = 32, return_sequences = TRUE) %>% layer_simple_rnn(units = 32) # This last layer only returns the last outputs. summary(model)

_______________________________________________________________________________________________________________
Layer (type)                                     Output Shape                                 Param #          
===============================================================================================================
embedding_8 (Embedding)                          (None, None, 32)                             320000           
_______________________________________________________________________________________________________________
simple_rnn_10 (SimpleRNN)                        (None, None, 32)                             2080             
_______________________________________________________________________________________________________________
simple_rnn_11 (SimpleRNN)                        (None, None, 32)                             2080             
_______________________________________________________________________________________________________________
simple_rnn_12 (SimpleRNN)                        (None, None, 32)                             2080             
_______________________________________________________________________________________________________________
simple_rnn_13 (SimpleRNN)                        (None, 32)                                   2080             
===============================================================================================================
Total params: 328,320
Trainable params: 328,320
Non-trainable params: 0
_______________________________________________________________________________________________________________

Teraz użyjmy takiego modelu w celu rozwiązania problemu klasyfikacji recenzji filmów. Zacznijmy od wstępnej obróbki danych:

rr library(keras) max_features <- 10000 # Number of words to consider as features maxlen <- 500 # Cuts off texts after this many words (among the max_features most common words) batch_size <- 32 cat(data…)

Loading data...

rr imdb <- dataset_imdb(num_words = max_features) c(c(input_train, y_train), c(input_test, y_test)) %<-% imdb cat(length(input_train), sequences)

25000 train sequences

rr cat(length(input_test), sequences)

25000 test sequences

rr cat(sequences (samples x time))

Pad sequences (samples x time)

rr input_train <- pad_sequences(input_train, maxlen = maxlen) input_test <- pad_sequences(input_test, maxlen = maxlen) cat(_train shape:, dim(input_train), \n)

input_train shape: 25000 500 

rr cat(_test shape:, dim(input_test), \n)

input_test shape: 25000 500 

Przeprowadźmy proces trenowania prostej rekurencyjnej sieci przy użyciu warstwy layer_embedding i warstwy layer_simple_rnn.

rr model <- keras_model_sequential() %>% layer_embedding(input_dim = max_features, output_dim = 32) %>% layer_simple_rnn(units = 32) %>% layer_dense(units = 1, activation = ) model %>% compile( optimizer = , loss = _crossentropy, metrics = c() ) history <- model %>% fit( input_train, y_train, epochs = 10, batch_size = 128, validation_split = 0.2 )

Teraz możemy wyświetlić wykresy dokładności i straty w procesach trenowania i walidacji:

rr plot(history)

Pierwsze maksymalnie uproszczone rozwiązanie, przedstawione w rozdziale 3., doprowadziło do uzyskania dokładności testowej na poziomie 88%. Niestety ta mała rekurencyjna sieć działa o wiele słabiej od wspomnianego rozwiązania (uzyskujemy dokładność walidacyjną na poziomie zaledwie 85%). Problem ten wynika częściowo z tego, że nie przetwarzamy pełnych sekwencji, a tylko 500 pierwszych słów, a więc sieć rekurencyjna ma dostęp do mniejszej ilości informacji niż model przedstawiony w rozdziale 3. Ponadto warstwa layer_simple_rnn nie sprawdza się najlepiej podczas przetwarzania długich sekwencji takich jak tekst. W takiej sytuacji lepiej sprawdzają się inne warstwy rekurencyjne. Przyjrzyjmy się bardziej zaawansowanym warstwom tego typu.

Warstwy LSTM i GRU

Czas przyjrzeć się praktycznemu przykładowi zastosowania warstwy layer_lstm. Skonfigurujemy model, w którym znajdzie się taka warstwa, i wytrenujemy go na zbiorze danych IMDB (patrz rysunek 6.13). Przypomina on zaprezentowany wcześniej model z warstwą layer_simple_rnn. Określimy tylko liczbę wymiarów obiektu wyjściowego warstwy layer_lstm. Pozostałe argumenty tej warstwy (jest ich wiele) pozostawimy przy wartościach domyślnych. Ustawienia domyślne pakietu Keras są przemyślane i zwykle „po prostu działają” bez konieczności poświęcania dużej ilości czasu na ręczne dostrajanie parametrów.

rr model <- keras_model_sequential() %>% layer_embedding(input_dim = max_features, output_dim = 32) %>% layer_lstm(units = 32) %>% layer_dense(units = 1, activation = ) model %>% compile( optimizer = , loss = _crossentropy, metrics = c() ) history <- model %>% fit( input_train, y_train, epochs = 10, batch_size = 128, validation_split = 0.2 )

rr plot(history)

LS0tDQp0aXRsZTogIlJla3VyZW5jeWpuZSBzaWVjaSBuZXVyb25vd2UiDQpvdXRwdXQ6IA0KICBodG1sX25vdGVib29rOiANCiAgICB0aGVtZTogY2VydWxlYW4NCiAgICBoaWdobGlnaHQ6IHRleHRtYXRlDQotLS0NCg0KYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQod2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UpDQpgYGANCg0KDQoNCiMjIFdhcnN0d2EgcmVrdXJlbmN5am5hIHcgcGFraWVjaWUgS2VyYXMNCg0KUHJvY2VzLCBrdMOzcnkgemFpbXBsZW1lbnRvd2FsacWbbXkgcHJ6eSB1xbx5Y2l1IGJpYmxpb3Rla2kgTnVtcHksIGplc3Qgb2Rwb3dpZWRuaWtpZW0gcHJvc3RlaiB3YXJzdHd5IG8gbmF6d2llIGxheWVyX3NpbXBsZV9ybm4gcGFraWV0dSBLZXJhczoNCg0KYGBge3IgZXZhbD1GQUxTRX0NCmxheWVyX3NpbXBsZV9ybm4odW5pdHMgPSAzMikNCmBgYA0KDQpQb21pxJlkenkgdHltaSBpbXBsZW1lbnRhY2phbWkgamVzdCBqZWRuYWsgcGV3bmEgbWHFgmEgcsOzxbxuaWNhOiB3YXJzdHdhIGxheWVyX3NpbXBsZV9ybm4gZHppZWxpIHNla3dlbmNqZSBuYSB3c2FkeSAob3BlcmFjasSZIHRha8SFIHd5a29udWrEhSB3c3p5c3RraWUgd2Fyc3R3eSBLZXJhcykg4oCUIG5pZSBwcnpldHdhcnphIHBvamVkeW5jenljaCBzZWt3ZW5jamkgdGFrLCBqYWsgdG8gbWlhxYJvIG1pZWpzY2UgdyBwcnp5a8WCYWR6aWUgemFpbXBsZW1lbnRvd2FueW0gemEgcG9tb2PEhSBwcm9zdGVnbyBrb2R1IFIuIFcgendpxIV6a3UgeiB0eW0gd2Fyc3R3YSB0YSBwcnp5am11amUgb2JpZWt0eSB3ZWrFm2Npb3dlIG8ga3N6dGHFgmNpZSAocm96bWlhcl93c2FkdSwga3Jva2lfY3phc3UsIGNlY2h5X3dlasWbY2lvd2UpLCBhIG5pZSAoa3Jva2lfY3phc3UsIGNlY2h5X3dlasWbY2lvd2UpLg0KDQpXYXJzdHdhIGxheWVyX3NpbXBsZV9ybm4sIHBvZG9ibmllIGphayB3c3p5c3RraWUgcmVrdXJlbmN5am5lIHdhcnN0d3kgcGFraWV0dSBLZXJhcywgbW/FvGUgYnnEhyB1cnVjaG9taW9uYSB3IGR3w7NjaCB0cnliYWNoOiBtb8W8ZSB6d3JhY2HEhyBwZcWCbmUgc2Vrd2VuY2plIGtvbGVqbnljaCBvYmlla3TDs3cgd3lqxZtjaW93eWNoIGRsYSBrYcW8ZGVnbyBrcm9rdSBjemFzdSAodHLDs2p3eW1pYXJvd2UgdGVuc29yeSBvIGtzenRhxYJjaWUgKHJvem1pYXJfd3NhZHUsIGtyb2tpX2N6YXN1LCBjZWNoeV93eWrFm2Npb3dlKSkgbHViIHR5bGtvIG9zdGF0bmllIG9iaWVrdHkgd3lqxZtjaW93ZSBwb3N6Y3plZ8OzbG55Y2ggc2Vrd2VuY2ppIHdlasWbY2lvd3ljaCAoZHd1d3ltaWFyb3dlIHRlbnNvcnkgbyBrc3p0YcWCY2llIChyb3ptaWFyX3dzYWR1LCBjZWNoeV93eWrFm2Npb3dlKSkuIFd5YsOzciB0cnlidSBwcmFjeSBqZXN0IGRva29ueXdhbnkgemEgcG9tb2PEhSBhcmd1bWVudHUgcmV0dXJuX3NlcXVlbmNlcy4gUHJ6ZWFuYWxpenVqbXkgcHJ6eWvFgmFkLCB3IGt0w7NyeW0gemFzdG9zb3dhbm8gd2Fyc3R3xJkgU2ltcGxlUk5OLCBhIGRhbmUgd3lqxZtjaW93ZSBzxIUgendyYWNhbmUgdHlsa28gcG9kY3phcyBwcnpldHdhcnphbmlhIG9zdGF0bmllZ28ga3Jva3UgY3phc3U6DQoNCg0KDQpgYGB7cn0NCmxpYnJhcnkoa2VyYXMpDQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lIA0KICBsYXllcl9lbWJlZGRpbmcoaW5wdXRfZGltID0gMTAwMDAsIG91dHB1dF9kaW0gPSAzMikgJT4lIA0KICBsYXllcl9zaW1wbGVfcm5uKHVuaXRzID0gMzIpDQoNCnN1bW1hcnkobW9kZWwpDQpgYGANCg0KYGBge3J9DQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lIA0KICBsYXllcl9lbWJlZGRpbmcoaW5wdXRfZGltID0gMTAwMDAsIG91dHB1dF9kaW0gPSAzMikgJT4lIA0KICBsYXllcl9zaW1wbGVfcm5uKHVuaXRzID0gMzIsIHJldHVybl9zZXF1ZW5jZXMgPSBUUlVFKQ0KDQpzdW1tYXJ5KG1vZGVsKQ0KYGBgDQoNCkN6YXNhbWkgd2FydG8gdXR3b3J6ecSHIHN0b3Mgc2vFgmFkYWrEhWN5IHNpxJkgeiBraWxrdSB3YXJzdHcgcmVrdXJlbmN5am55Y2guIFp3acSZa3N6YSB0byBzacWCxJkgdHdvcnplbmlhIHJlcHJlemVudGFjamkgcHJ6ZXogc2llxIcuIFByenkgdGFraWVqIGtvbmZpZ3VyYWNqaSB3c3p5c3RraWUgd2Fyc3R3eSBwb8WbcmVkbmllIG11c3rEhSB6d3JhY2HEhyBwZcWCbsSFIHNla3dlbmNqxJkgb2JpZWt0w7N3IHd5asWbY2lvd3ljaDoNCg0KYGBge3J9DQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lIA0KICBsYXllcl9lbWJlZGRpbmcoaW5wdXRfZGltID0gMTAwMDAsIG91dHB1dF9kaW0gPSAzMikgJT4lIA0KICBsYXllcl9zaW1wbGVfcm5uKHVuaXRzID0gMzIsIHJldHVybl9zZXF1ZW5jZXMgPSBUUlVFKSAlPiUgDQogIGxheWVyX3NpbXBsZV9ybm4odW5pdHMgPSAzMiwgcmV0dXJuX3NlcXVlbmNlcyA9IFRSVUUpICU+JQ0KICBsYXllcl9zaW1wbGVfcm5uKHVuaXRzID0gMzIsIHJldHVybl9zZXF1ZW5jZXMgPSBUUlVFKSAlPiUNCiAgbGF5ZXJfc2ltcGxlX3Jubih1bml0cyA9IDMyKSAgIyBPc3RhdG5pYSB3YXJzdHdhIHp3cmFjYSB0eWxrbyBvc3RhdG5pIG9iaWVrdCB3eWrFm2Npb3d5Lg0KDQpzdW1tYXJ5KG1vZGVsKQ0KYGBgDQoNClRlcmF6IHXFvHlqbXkgdGFraWVnbyBtb2RlbHUgdyBjZWx1IHJvendpxIV6YW5pYSBwcm9ibGVtdSBrbGFzeWZpa2FjamkgcmVjZW56amkgZmlsbcOzdy4gWmFjem5pam15IG9kIHdzdMSZcG5laiBvYnLDs2JraSBkYW55Y2g6DQoNCmBgYHtyfQ0KbGlicmFyeShrZXJhcykNCg0KbWF4X2ZlYXR1cmVzIDwtIDEwMDAwICAjIExpY3piYSBzxYLDs3cgdHJha3Rvd2FueWNoIGpha28gY2VjaHkuDQptYXhsZW4gPC0gNTAwICAjIFVjaW5hIHJlY2VuemplIHBvIHRlaiBsaWN6YmllIHPFgsOzdyBuYWxlxbzEhWN5Y2ggZG8gemJpb3J1IG1heF9mZWF0dXJlcyBzxYLDs3cgbmFqY3rEmcWbY2llaiB3eXN0xJlwdWrEhWN5Y2ggdyB6YmlvcnplLg0KYmF0Y2hfc2l6ZSA8LSAzMg0KDQpjYXQoIsWBYWRvd2FuaWUgZGFueWNoLi4uXG4iKQ0KaW1kYiA8LSBkYXRhc2V0X2ltZGIobnVtX3dvcmRzID0gbWF4X2ZlYXR1cmVzKQ0KYyhjKGlucHV0X3RyYWluLCB5X3RyYWluKSwgYyhpbnB1dF90ZXN0LCB5X3Rlc3QpKSAlPC0lIGltZGIgDQpjYXQobGVuZ3RoKGlucHV0X3RyYWluKSwgInNla3dlbmNqZSB0cmVuaW5nb3dlXG4iKQ0KY2F0KGxlbmd0aChpbnB1dF90ZXN0KSwgInNla3dlbmNqZSB0ZXN0b3dlIikNCg0KY2F0KCJTZWt3ZW5jamUgKHByw7Nia2kgeCBjemFzKVxuIikNCmlucHV0X3RyYWluIDwtIHBhZF9zZXF1ZW5jZXMoaW5wdXRfdHJhaW4sIG1heGxlbiA9IG1heGxlbikNCmlucHV0X3Rlc3QgPC0gcGFkX3NlcXVlbmNlcyhpbnB1dF90ZXN0LCBtYXhsZW4gPSBtYXhsZW4pDQpjYXQoIktzenRhxYJ0IG9iaWVrdHUgaW5wdXRfdHJhaW46IiwgZGltKGlucHV0X3RyYWluKSwgIlxuIikNCmNhdCgiS3N6dGHFgnQgb2JpZWt0dSBpbnB1dF90ZXN0OiIsIGRpbShpbnB1dF90ZXN0KSwgIlxuIikNCmBgYA0KDQpQcnplcHJvd2FkxbpteSBwcm9jZXMgdHJlbm93YW5pYSBwcm9zdGVqIHJla3VyZW5jeWpuZWogc2llY2kgcHJ6eSB1xbx5Y2l1IHdhcnN0d3kgbGF5ZXJfZW1iZWRkaW5nIGkgd2Fyc3R3eSBsYXllcl9zaW1wbGVfcm5uLg0KDQpgYGB7ciwgZWNobz1UUlVFLCByZXN1bHRzPSdoaWRlJ30NCm1vZGVsIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUNCiAgbGF5ZXJfZW1iZWRkaW5nKGlucHV0X2RpbSA9IG1heF9mZWF0dXJlcywgb3V0cHV0X2RpbSA9IDMyKSAlPiUNCiAgbGF5ZXJfc2ltcGxlX3Jubih1bml0cyA9IDMyKSAlPiUNCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxLCBhY3RpdmF0aW9uID0gInNpZ21vaWQiKQ0KDQptb2RlbCAlPiUgY29tcGlsZSgNCiAgb3B0aW1pemVyID0gInJtc3Byb3AiLA0KICBsb3NzID0gImJpbmFyeV9jcm9zc2VudHJvcHkiLA0KICBtZXRyaWNzID0gYygiYWNjIikNCikNCg0KaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KA0KICBpbnB1dF90cmFpbiwgeV90cmFpbiwNCiAgZXBvY2hzID0gMTAsDQogIGJhdGNoX3NpemUgPSAxMjgsDQogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjINCikNCmBgYA0KDQpUZXJheiBtb8W8ZW15IHd5xZt3aWV0bGnEhyB3eWtyZXN5IGRva8WCYWRub8WbY2kgaSBzdHJhdHkgdyBwcm9jZXNhY2ggdHJlbm93YW5pYSBpIHdhbGlkYWNqaToNCg0KYGBge3J9DQpwbG90KGhpc3RvcnkpDQpgYGANCg0KUGllcndzemUgbWFrc3ltYWxuaWUgdXByb3N6Y3pvbmUgcm96d2nEhXphbmllLCBwcnplZHN0YXdpb25lIHcgcm96ZHppYWxlIDMuLCBkb3Byb3dhZHppxYJvIGRvIHV6eXNrYW5pYSBkb2vFgmFkbm/Fm2NpIHRlc3Rvd2VqIG5hIHBvemlvbWllIDg4JS4gTmllc3RldHkgdGEgbWHFgmEgcmVrdXJlbmN5am5hIHNpZcSHIGR6aWHFgmEgbyB3aWVsZSBzxYJhYmllaiBvZCB3c3BvbW5pYW5lZ28gcm96d2nEhXphbmlhICh1enlza3VqZW15IGRva8WCYWRub8WbxIcgd2FsaWRhY3lqbsSFIG5hIHBvemlvbWllIHphbGVkd2llIDg1JSkuIFByb2JsZW0gdGVuIHd5bmlrYSBjesSZxZtjaW93byB6IHRlZ28sIMW8ZSBuaWUgcHJ6ZXR3YXJ6YW15IHBlxYJueWNoIHNla3dlbmNqaSwgYSB0eWxrbyA1MDAgcGllcndzenljaCBzxYLDs3csIGEgd2nEmWMgc2llxIcgcmVrdXJlbmN5am5hIG1hIGRvc3TEmXAgZG8gbW5pZWpzemVqIGlsb8WbY2kgaW5mb3JtYWNqaSBuacW8IG1vZGVsIHByemVkc3Rhd2lvbnkgdyByb3pkemlhbGUgMy4gUG9uYWR0byB3YXJzdHdhIGxheWVyX3NpbXBsZV9ybm4gbmllIHNwcmF3ZHphIHNpxJkgbmFqbGVwaWVqIHBvZGN6YXMgcHJ6ZXR3YXJ6YW5pYSBkxYJ1Z2ljaCBzZWt3ZW5jamkgdGFraWNoIGphayB0ZWtzdC4gVyB0YWtpZWogc3l0dWFjamkgbGVwaWVqIHNwcmF3ZHphasSFIHNpxJkgaW5uZSB3YXJzdHd5IHJla3VyZW5jeWpuZS4gUHJ6eWpyenlqbXkgc2nEmSBiYXJkemllaiB6YWF3YW5zb3dhbnltIHdhcnN0d29tIHRlZ28gdHlwdS4NCg0KIyMgV2Fyc3R3eSBMU1RNIGkgR1JVDQoNCkN6YXMgcHJ6eWpyemXEhyBzacSZIHByYWt0eWN6bmVtdSBwcnp5a8WCYWRvd2kgemFzdG9zb3dhbmlhIHdhcnN0d3kgbGF5ZXJfbHN0bS4gU2tvbmZpZ3VydWplbXkgbW9kZWwsIHcga3TDs3J5bSB6bmFqZHppZSBzacSZIHRha2Egd2Fyc3R3YSwgaSB3eXRyZW51amVteSBnbyBuYSB6YmlvcnplIGRhbnljaCBJTURCIChwYXRyeiByeXN1bmVrIDYuMTMpLiBQcnp5cG9taW5hIG9uIHphcHJlemVudG93YW55IHdjemXFm25pZWogbW9kZWwgeiB3YXJzdHfEhSBsYXllcl9zaW1wbGVfcm5uLiBPa3JlxZtsaW15IHR5bGtvIGxpY3pixJkgd3ltaWFyw7N3IG9iaWVrdHUgd3lqxZtjaW93ZWdvIHdhcnN0d3kgbGF5ZXJfbHN0bS4gUG96b3N0YcWCZSBhcmd1bWVudHkgdGVqIHdhcnN0d3kgKGplc3QgaWNoIHdpZWxlKSBwb3pvc3Rhd2lteSBwcnp5IHdhcnRvxZtjaWFjaCBkb215xZtsbnljaC4gVXN0YXdpZW5pYSBkb215xZtsbmUgcGFraWV0dSBLZXJhcyBzxIUgcHJ6ZW15xZtsYW5lIGkgend5a2xlIOKAnnBvIHByb3N0dSBkemlhxYJhasSF4oCdIGJleiBrb25pZWN6bm/Fm2NpIHBvxZt3acSZY2FuaWEgZHXFvGVqIGlsb8WbY2kgY3phc3UgbmEgcsSZY3puZSBkb3N0cmFqYW5pZSBwYXJhbWV0csOzdy4NCg0KYGBge3IsIGVjaG89VFJVRSwgcmVzdWx0cz0naGlkZSd9DQptb2RlbCA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCkgJT4lIA0KICBsYXllcl9lbWJlZGRpbmcoaW5wdXRfZGltID0gbWF4X2ZlYXR1cmVzLCBvdXRwdXRfZGltID0gMzIpICU+JSANCiAgbGF5ZXJfbHN0bSh1bml0cyA9IDMyKSAlPiUgDQogIGxheWVyX2RlbnNlKHVuaXRzID0gMSwgYWN0aXZhdGlvbiA9ICJzaWdtb2lkIikNCg0KbW9kZWwgJT4lIGNvbXBpbGUoDQogIG9wdGltaXplciA9ICJybXNwcm9wIiwgDQogIGxvc3MgPSAiYmluYXJ5X2Nyb3NzZW50cm9weSIsIA0KICBtZXRyaWNzID0gYygiYWNjIikNCikNCg0KaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KA0KICBpbnB1dF90cmFpbiwgeV90cmFpbiwNCiAgZXBvY2hzID0gMTAsDQogIGJhdGNoX3NpemUgPSAxMjgsDQogIHZhbGlkYXRpb25fc3BsaXQgPSAwLjINCikNCmBgYA0KDQpgYGB7cn0NCnBsb3QoaGlzdG9yeSkNCmBgYA==