Przyjrzyjmy się pierwszemu przykładowi sieci neuronowej korzystającej z biblioteki Keras w języku R. Sieć ta ma klasyfikować ręcznie zapisane cyfry. Nie przejmuj się, jeżeli nie zrozumiesz wszystkich elementów tego przykładu. Jest to normalne, jeżeli nie masz doświadczenia w pracy z biblioteką Keras ani innym podobnym do niej pakietem. Prawdopodobnie jeszcze nawet nie zainstalowałeś pakietu Keras… To niczemu nie przeszkadza. W kolejnym rozdziale opiszę każdy element tego przykładu w sposób szczegółowy. W związku z tym nie przejmuj się, jeżeli niektóre rzeczy wydadzą Ci się czarną magią! Musimy od czegoś zacząć.

W zaprezentowanym przykładzie próbujemy rozwiązać problem klasyfikacji obrazów w skali szarości przedstawiających ręcznie zapisane cyfry (obrazy te mają rozdzielczość 28???28 pikseli). Chcemy podzielić je na 10 kategorii (cyfry od 0 do 9). Będziemy korzystać ze zbioru danych MNIST, który jest uznawany przez środowisko analityków za zbiór klasyczny. Istnieje on tak długo, jak długa jest historia uczenia maszynowego. Zbiór ten zawiera 60 000 obrazów treningowych oraz 10 000 obrazów testowych. Został utworzony przez Narodowy Instytut Standaryzacji i Technologii (NIST) w latach 80. ubiegłego wieku. Rozwiązanie wspomnianego problemu można porównać do wyświetlenia napisu „Witaj, świecie!” podczas nauki nowego języka programowania. Zbiór ten jest również używany w celu sprawdzania tego, czy algorytm działa poprawnie. Jeżeli zaczniesz zawodowo zajmować się uczeniem maszynowym, to odkryjesz, że zbiór MNIST pojawia się ciągle w różnych pracach naukowych, artykułach publikowanych w internecie itd.

Zbiór danych MNIST jest dołączony do pakietu Keras w formie list train i test. Każda z tych list składa się z obrazów (x) i związanych z nimi etykiet (y):

Tablice train_images i train_labels tworzą treningowy zbiór danych. Będzie on używany podczas trenowania modelu. Do testowania posłuży nam testowy zbiór danych, składający się z tablic test_images i test_labels. Obrazy są zakodowane w formie tablic Numpy, a etykiety mają formę tablicy cyfr (od 0 do 9). Do każdego obrazu przypisana jest tylko jedna etykieta.

Funkcja str() języka R umożliwia szybkie przyjrzenie się strukturze tablicy. Skorzystajmy z niej w celu przeanalizowania danych treningowych:

rr str(train_images)

rr str(train_labels)

A teraz zobaczmy, jak wyglądają dane testowe:

rr str(test_images)

rr str(test_labels)

Będziemy pracować według następującego przepływu roboczego: najpierw będziemy trenować sieć neuronową na danych treningowych: train_images i train_labels. Sieć nauczy się kojarzyć obrazy i etykiety. Następnie nasza sieć wygeneruje przewidywania dotyczące zbioru test_images, a uzyskane wyniki porównamy z etykietami test_labels.

Zbudujmy naszą sieć. Przypominam, że nie musisz jeszcze rozumieć wszystkiego, co się dzieje w tym przykładzie.

rr network <- keras_model_sequential() %>% layer_dense(units = 512, activation = , input_shape = c(28 * 28)) %>% layer_dense(units = 10, activation = )

Głównym blokiem składowym sieci neuronowej jest warstwa (ang. layer). Jest to moduł przetwarzania danych, który można traktować jako filtr danych. Dane wychodzące z filtra mają bardziej przydatną formę od danych do niego wchodzących. Niektóre warstwy dokonują ekstrakcji reprezentacji kierowanych do nich danych — reprezentacje te powinny ułatwiać rozwiązanie problemu, z którym się zmagamy. Większość uczenia głębokiego składa się z łączenia ze sobą prostych warstw w celu zaimplementowania progresywnej destylacji danych. Model uczenia głębokiego jest jak sito przetwarzające dane składające się z coraz drobniejszych siatek — warstw.

Nasza sieć składa się z sekwencji dwóch warstw Dense, które są ze sobą połączone w sposób gęsty (dochodzi tu do gęstego połączenia). Druga warstwa jest dziesięcioelementową warstwą softmax — warstwa ta zwróci tablicę 10 wartości prawdopodobieństwa (suma wszystkich tych wartości jest równa 1). Każdy z tych wyników określa prawdopodobieństwo tego, że na danym obrazie przedstawiono daną cyfrę (obraz może przedstawiać jedną z dziesięciu cyfr).

Na etapie kompilacji musimy określić jeszcze trzy rzeczy w celu przygotowania sieci do trenowania. Są to:

W kolejnych dwóch rozdziałach wyjaśnię cel stosowania funkcji straty i optymalizatora.

rr network %>% compile( optimizer = , loss = _crossentropy, metrics = c() )

Zanim rozpoczniemy trenowanie, zmienimy kształt danych tak, aby przyjęły kształt oczekiwany przez sieć, i przeskalujemy je do wartości z zakresu [0, 1]. Początkowo nasze obrazy treningowe były zapisywane w postaci macierzy o wymiarach (60000, 28, 28), zawierającej wartości z zakresu [0, 255], i typie uint8. Przekształcamy je w tablicę typu float32 o wymiarach (60000, 28 * 28), zawierającą wartości od 0 do 1.

rr train_images <- array_reshape(train_images, c(60000, 28 * 28)) train_images <- train_images / 255

test_images <- array_reshape(test_images, c(10000, 28 * 28)) test_images <- test_images / 255

Musimy dodatkowo zakodować etykiety za pomocą kategorii (proces ten wyjaśnię w rozdziale 3.):

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

Jesteśmy gotowi do uruchomienia trenowania sieci. Korzystamy z pakietu Keras, a więc odwołamy się do metody fit modelu (dopasujemy model do danych treningowych):

Podczas trenowania wyświetlane są dwie wartości: strata sieci i jej dokładność (obie wartości dotyczą treningowego zbioru danych).

W czasie trenowania szybko osiągamy dokładność 0,989 (98,9%). Teraz możemy sprawdzić dokładność przetwarzania testowego zbioru danych:

rr metrics <- network %>% evaluate(test_images, test_labels, verbose = 0) metrics

W przypadku testowego zbioru danych uzyskaliśmy dokładność na poziomie 97,8%, a więc wartość nieco niższą niż dla zbioru treningowego. Różnica między tymi wartościami wynika z nadmiernego dopasowania. Modele uczenia maszynowego mają tendencję do niższej dokładności przetwarzania nowych danych, niż to miało miejsce w przypadku danych treningowych. Zagadnienie to jest głównym tematem rozdziału 3.

To tyle, jeżeli chodzi o nasz pierwszy przykład — właśnie zobaczyłeś, że zbudowanie i wytrenowanie sieci neuronowej klasyfikującej zapis ręczny cyfr może zająć mniej niż 20 linii kodu R. W kolejnym rozdziale opiszę szczegółowo wszystkie linie tego kodu i wyjaśnię, jakie operacje są wykonywane w tle. Dowiesz się, czym są tensory, obiekty przeznaczone do przechowywania danych sieci i operacje na tensorach. Poznasz budowę warstw sieci i algorytm spadku gradientowego, który umożliwia sieci uczenie się na podstawie treningowego zbioru danych.

LS0tDQp0aXRsZTogIk1hdGVtYXR5Y3puZSBwb2RzdGF3eSBzaWVjaSBuZXVyb25vd3ljaCINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6IA0KICAgIHRoZW1lOiBjZXJ1bGVhbg0KICAgIGhpZ2hsaWdodDogdGV4dG1hdGUNCi0tLQ0KDQpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldCh3YXJuaW5nID0gRkFMU0UsIG1lc3NhZ2UgPSBGQUxTRSkNCmBgYA0KDQoNClByenlqcnp5am15IHNpxJkgcGllcndzemVtdSBwcnp5a8WCYWRvd2kgc2llY2kgbmV1cm9ub3dlaiBrb3J6eXN0YWrEhWNlaiB6IGJpYmxpb3Rla2kgS2VyYXMgdyBqxJl6eWt1IFIuIFNpZcSHIHRhIG1hIGtsYXN5Zmlrb3dhxIcgcsSZY3puaWUgemFwaXNhbmUgY3lmcnkuIE5pZSBwcnplam11aiBzacSZLCBqZcW8ZWxpIG5pZSB6cm96dW1pZXN6IHdzenlzdGtpY2ggZWxlbWVudMOzdyB0ZWdvIHByenlrxYJhZHUuIEplc3QgdG8gbm9ybWFsbmUsIGplxbxlbGkgbmllIG1hc3ogZG/Fm3dpYWRjemVuaWEgdyBwcmFjeSB6IGJpYmxpb3Rla8SFIEtlcmFzIGFuaSBpbm55bSBwb2RvYm55bSBkbyBuaWVqIHBha2lldGVtLiBQcmF3ZG9wb2RvYm5pZSBqZXN6Y3plIG5hd2V0IG5pZSB6YWluc3RhbG93YcWCZcWbIHBha2lldHUgS2VyYXPigKYgVG8gbmljemVtdSBuaWUgcHJ6ZXN6a2FkemEuIFcga29sZWpueW0gcm96ZHppYWxlIG9waXN6xJkga2HFvGR5IGVsZW1lbnQgdGVnbyBwcnp5a8WCYWR1IHcgc3Bvc8OzYiBzemN6ZWfDs8WCb3d5LiBXIHp3acSFemt1IHogdHltIG5pZSBwcnplam11aiBzacSZLCBqZcW8ZWxpIG5pZWt0w7NyZSByemVjenkgd3lkYWR6xIUgQ2kgc2nEmSBjemFybsSFIG1hZ2nEhSEgTXVzaW15IG9kIGN6ZWdvxZsgemFjesSFxIcuDQoNClcgemFwcmV6ZW50b3dhbnltIHByenlrxYJhZHppZSBwcsOzYnVqZW15IHJvendpxIV6YcSHIHByb2JsZW0ga2xhc3lmaWthY2ppIG9icmF6w7N3IHcgc2thbGkgc3phcm/Fm2NpIHByemVkc3Rhd2lhasSFY3ljaCByxJljem5pZSB6YXBpc2FuZSBjeWZyeSAob2JyYXp5IHRlIG1hasSFIHJvemR6aWVsY3pvxZvEhyAyOD8/PzI4IHBpa3NlbGkpLiBDaGNlbXkgcG9kemllbGnEhyBqZSBuYSAxMCBrYXRlZ29yaWkgKGN5ZnJ5IG9kIDAgZG8gOSkuIELEmWR6aWVteSBrb3J6eXN0YcSHIHplIHpiaW9ydSBkYW55Y2ggTU5JU1QsIGt0w7NyeSBqZXN0IHV6bmF3YW55IHByemV6IMWbcm9kb3dpc2tvIGFuYWxpdHlrw7N3IHphIHpiacOzciBrbGFzeWN6bnkuIElzdG5pZWplIG9uIHRhayBkxYJ1Z28sIGphayBkxYJ1Z2EgamVzdCBoaXN0b3JpYSB1Y3plbmlhIG1hc3p5bm93ZWdvLiBaYmnDs3IgdGVuIHphd2llcmEgNjAgMDAwIG9icmF6w7N3IHRyZW5pbmdvd3ljaCBvcmF6IDEwIDAwMCBvYnJhesOzdyB0ZXN0b3d5Y2guIFpvc3RhxYIgdXR3b3J6b255IHByemV6IE5hcm9kb3d5IEluc3R5dHV0IFN0YW5kYXJ5emFjamkgaSBUZWNobm9sb2dpaSAoTklTVCkgdyBsYXRhY2ggODAuIHViaWVnxYJlZ28gd2lla3UuIFJvendpxIV6YW5pZSB3c3BvbW5pYW5lZ28gcHJvYmxlbXUgbW/FvG5hIHBvcsOzd25hxIcgZG8gd3nFm3dpZXRsZW5pYSBuYXBpc3Ug4oCeV2l0YWosIMWbd2llY2llIeKAnSBwb2RjemFzIG5hdWtpIG5vd2VnbyBqxJl6eWthIHByb2dyYW1vd2FuaWEuIFpiacOzciB0ZW4gamVzdCByw7N3bmllxbwgdcW8eXdhbnkgdyBjZWx1IHNwcmF3ZHphbmlhIHRlZ28sIGN6eSBhbGdvcnl0bSBkemlhxYJhIHBvcHJhd25pZS4gSmXFvGVsaSB6YWN6bmllc3ogemF3b2Rvd28gemFqbW93YcSHIHNpxJkgdWN6ZW5pZW0gbWFzenlub3d5bSwgdG8gb2RrcnlqZXN6LCDFvGUgemJpw7NyIE1OSVNUIHBvamF3aWEgc2nEmSBjacSFZ2xlIHcgcsOzxbxueWNoIHByYWNhY2ggbmF1a293eWNoLCBhcnR5a3XFgmFjaCBwdWJsaWtvd2FueWNoIHcgaW50ZXJuZWNpZSBpdGQuIA0KDQpaYmnDs3IgZGFueWNoIE1OSVNUIGplc3QgZG/FgsSFY3pvbnkgZG8gcGFraWV0dSBLZXJhcyB3IGZvcm1pZSBsaXN0IHRyYWluIGkgdGVzdC4gS2HFvGRhIHogdHljaCBsaXN0IHNrxYJhZGEgc2nEmSB6IG9icmF6w7N3ICh4KSBpIHp3acSFemFueWNoIHogbmltaSBldHlraWV0ICh5KToNCg0KYGBge3IsIHJlc3VsdHM9J2hpZGUnfQ0KbGlicmFyeShrZXJhcykNCg0KbW5pc3QgPC0gZGF0YXNldF9tbmlzdCgpDQp0cmFpbl9pbWFnZXMgPC0gbW5pc3QkdHJhaW4keA0KdHJhaW5fbGFiZWxzIDwtIG1uaXN0JHRyYWluJHkNCnRlc3RfaW1hZ2VzIDwtIG1uaXN0JHRlc3QkeA0KdGVzdF9sYWJlbHMgPC0gbW5pc3QkdGVzdCR5DQpgYGANCg0KVGFibGljZSB0cmFpbl9pbWFnZXMgaSB0cmFpbl9sYWJlbHMgdHdvcnrEhSB0cmVuaW5nb3d5IHpiacOzciBkYW55Y2guIELEmWR6aWUgb24gdcW8eXdhbnkgcG9kY3phcyB0cmVub3dhbmlhIG1vZGVsdS4gRG8gdGVzdG93YW5pYSBwb3PFgnXFvHkgbmFtIHRlc3Rvd3kgemJpw7NyIGRhbnljaCwgc2vFgmFkYWrEhWN5IHNpxJkgeiB0YWJsaWMgdGVzdF9pbWFnZXMgaSB0ZXN0X2xhYmVscy4gT2JyYXp5IHPEhSB6YWtvZG93YW5lIHcgZm9ybWllIHRhYmxpYyBOdW1weSwgYSBldHlraWV0eSBtYWrEhSBmb3JtxJkgdGFibGljeSBjeWZyIChvZCAwIGRvIDkpLiBEbyBrYcW8ZGVnbyBvYnJhenUgcHJ6eXBpc2FuYSBqZXN0IHR5bGtvIGplZG5hIGV0eWtpZXRhLg0KDQpGdW5rY2phIHN0cigpIGrEmXp5a2EgUiB1bW/FvGxpd2lhIHN6eWJraWUgcHJ6eWpyemVuaWUgc2nEmSBzdHJ1a3R1cnplIHRhYmxpY3kuIFNrb3J6eXN0YWpteSB6IG5pZWogdyBjZWx1IHByemVhbmFsaXpvd2FuaWEgZGFueWNoIHRyZW5pbmdvd3ljaDoNCg0KYGBge3J9DQpzdHIodHJhaW5faW1hZ2VzKQ0KYGBgDQoNCmBgYHtyfQ0Kc3RyKHRyYWluX2xhYmVscykNCmBgYA0KDQpBIHRlcmF6IHpvYmFjem15LCBqYWsgd3lnbMSFZGFqxIUgZGFuZSB0ZXN0b3dlOg0KDQpgYGB7cn0NCnN0cih0ZXN0X2ltYWdlcykNCmBgYA0KDQpgYGB7cn0NCnN0cih0ZXN0X2xhYmVscykNCmBgYA0KDQpCxJlkemllbXkgcHJhY293YcSHIHdlZMWCdWcgbmFzdMSZcHVqxIVjZWdvIHByemVwxYJ5d3Ugcm9ib2N6ZWdvOiBuYWpwaWVydyBixJlkemllbXkgdHJlbm93YcSHIHNpZcSHIG5ldXJvbm93xIUgbmEgZGFueWNoIHRyZW5pbmdvd3ljaDogdHJhaW5faW1hZ2VzIGkgdHJhaW5fbGFiZWxzLiBTaWXEhyBuYXVjenkgc2nEmSBrb2phcnp5xIcgb2JyYXp5IGkgZXR5a2lldHkuIE5hc3TEmXBuaWUgbmFzemEgc2llxIcgd3lnZW5lcnVqZSBwcnpld2lkeXdhbmlhIGRvdHljesSFY2UgemJpb3J1IHRlc3RfaW1hZ2VzLCBhIHV6eXNrYW5lIHd5bmlraSBwb3LDs3duYW15IHogZXR5a2lldGFtaSB0ZXN0X2xhYmVscy4NCg0KWmJ1ZHVqbXkgbmFzesSFIHNpZcSHLiBQcnp5cG9taW5hbSwgxbxlIG5pZSBtdXNpc3ogamVzemN6ZSByb3p1bWllxIcgd3N6eXN0a2llZ28sIGNvIHNpxJkgZHppZWplIHcgdHltIHByenlrxYJhZHppZS4NCg0KYGBge3J9DQpuZXR3b3JrIDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSAlPiUgDQogIGxheWVyX2RlbnNlKHVuaXRzID0gNTEyLCBhY3RpdmF0aW9uID0gInJlbHUiLCBpbnB1dF9zaGFwZSA9IGMoMjggKiAyOCkpICU+JSANCiAgbGF5ZXJfZGVuc2UodW5pdHMgPSAxMCwgYWN0aXZhdGlvbiA9ICJzb2Z0bWF4IikNCmBgYA0KDQpHxYLDs3dueW0gYmxva2llbSBza8WCYWRvd3ltIHNpZWNpIG5ldXJvbm93ZWogamVzdCB3YXJzdHdhIChhbmcuIGxheWVyKS4gSmVzdCB0byBtb2R1xYIgcHJ6ZXR3YXJ6YW5pYSBkYW55Y2gsIGt0w7NyeSBtb8W8bmEgdHJha3Rvd2HEhyBqYWtvIGZpbHRyIGRhbnljaC4gRGFuZSB3eWNob2R6xIVjZSB6IGZpbHRyYSBtYWrEhSBiYXJkemllaiBwcnp5ZGF0bsSFIGZvcm3EmSBvZCBkYW55Y2ggZG8gbmllZ28gd2Nob2R6xIVjeWNoLiBOaWVrdMOzcmUgd2Fyc3R3eSBkb2tvbnVqxIUgZWtzdHJha2NqaSByZXByZXplbnRhY2ppIGtpZXJvd2FueWNoIGRvIG5pY2ggZGFueWNoIOKAlCByZXByZXplbnRhY2plIHRlIHBvd2lubnkgdcWCYXR3aWHEhyByb3p3acSFemFuaWUgcHJvYmxlbXUsIHoga3TDs3J5bSBzacSZIHptYWdhbXkuIFdpxJlrc3pvxZvEhyB1Y3plbmlhIGfFgsSZYm9raWVnbyBza8WCYWRhIHNpxJkgeiDFgsSFY3plbmlhIHplIHNvYsSFIHByb3N0eWNoIHdhcnN0dyB3IGNlbHUgemFpbXBsZW1lbnRvd2FuaWEgcHJvZ3Jlc3l3bmVqIGRlc3R5bGFjamkgZGFueWNoLiBNb2RlbCB1Y3plbmlhIGfFgsSZYm9raWVnbyBqZXN0IGphayBzaXRvIHByemV0d2FyemFqxIVjZSBkYW5lIHNrxYJhZGFqxIVjZSBzacSZIHogY29yYXogZHJvYm5pZWpzenljaCBzaWF0ZWsg4oCUIHdhcnN0dy4NCg0KTmFzemEgc2llxIcgc2vFgmFkYSBzacSZIHogc2Vrd2VuY2ppIGR3w7NjaCB3YXJzdHcgRGVuc2UsIGt0w7NyZSBzxIUgemUgc29ixIUgcG/FgsSFY3pvbmUgdyBzcG9zw7NiIGfEmXN0eSAoZG9jaG9kemkgdHUgZG8gZ8SZc3RlZ28gcG/FgsSFY3plbmlhKS4gRHJ1Z2Egd2Fyc3R3YSBqZXN0IGR6aWVzacSZY2lvZWxlbWVudG93xIUgd2Fyc3R3xIUgc29mdG1heCDigJQgd2Fyc3R3YSB0YSB6d3LDs2NpIHRhYmxpY8SZIDEwIHdhcnRvxZtjaSBwcmF3ZG9wb2RvYmllxYRzdHdhIChzdW1hIHdzenlzdGtpY2ggdHljaCB3YXJ0b8WbY2kgamVzdCByw7N3bmEgMSkuIEthxbxkeSB6IHR5Y2ggd3luaWvDs3cgb2tyZcWbbGEgcHJhd2RvcG9kb2JpZcWEc3R3byB0ZWdvLCDFvGUgbmEgZGFueW0gb2JyYXppZSBwcnplZHN0YXdpb25vIGRhbsSFIGN5ZnLEmSAob2JyYXogbW/FvGUgcHJ6ZWRzdGF3aWHEhyBqZWRuxIUgeiBkemllc2nEmWNpdSBjeWZyKS4NCg0KTmEgZXRhcGllIGtvbXBpbGFjamkgbXVzaW15IG9rcmXFm2xpxIcgamVzemN6ZSB0cnp5IHJ6ZWN6eSB3IGNlbHUgcHJ6eWdvdG93YW5pYSBzaWVjaSBkbyB0cmVub3dhbmlhLiBTxIUgdG86DQoNCiogRnVua2NqYSBzdHJhdHkg4oCUIGZ1bmtjamEgdGEgZGVmaW5pdWplIHNwb3PDs2IgcG9taWFydSB3eWRham5vxZtjaSBzaWVjaSBwb2RjemFzIHByemV0d2FyemFuaWEgdHJlbmluZ293ZWdvIHpiaW9ydSBkYW55Y2gsIGEgd2nEmWMgcG96d2FsYSBuYSBkb3N0cmFqYW5pZSBwYXJhbWV0csOzdyBzaWVjaSB3ZSB3xYJhxZtjaXd5bSBraWVydW5rdS4NCiogT3B0eW1hbGl6YXRvciDigJQgbWVjaGFuaXptIGRvc3RyYWphbmlhIHNpZWNpIG5hIHBvZHN0YXdpZSBkYW55Y2ggendyYWNhbnljaCBwcnpleiBmdW5rY2rEmSBzdHJhdHkuDQoqIE1ldHJ5a2kgbW9uaXRvcm93YW5lIHBvZGN6YXMgdHJlbm93YW5pYSBpIHRlc3Rvd2FuaWEg4oCUIHR1dGFqIGludGVyZXN1amUgbmFzIGplZHluaWUgZG9rxYJhZG5vxZvEhyAoY3rEmcWbxIcgb2JyYXrDs3csIGt0w7NyYSB6b3N0YcWCYSB3xYJhxZtjaXdpZSBza2xhc3lmaWtvd2FuYSkuDQoNClcga29sZWpueWNoIGR3w7NjaCByb3pkemlhxYJhY2ggd3lqYcWbbmnEmSBjZWwgc3Rvc293YW5pYSBmdW5rY2ppIHN0cmF0eSBpIG9wdHltYWxpemF0b3JhLg0KDQpgYGB7cn0NCm5ldHdvcmsgJT4lIGNvbXBpbGUoDQogIG9wdGltaXplciA9ICJybXNwcm9wIiwNCiAgbG9zcyA9ICJjYXRlZ29yaWNhbF9jcm9zc2VudHJvcHkiLA0KICBtZXRyaWNzID0gYygiYWNjdXJhY3kiKQ0KKQ0KYGBgDQoNClphbmltIHJvenBvY3puaWVteSB0cmVub3dhbmllLCB6bWllbmlteSBrc3p0YcWCdCBkYW55Y2ggdGFrLCBhYnkgcHJ6eWrEmcWCeSBrc3p0YcWCdCBvY3pla2l3YW55IHByemV6IHNpZcSHLCBpIHByemVza2FsdWplbXkgamUgZG8gd2FydG/Fm2NpIHogemFrcmVzdSBbMCwgMV0uIFBvY3rEhXRrb3dvIG5hc3plIG9icmF6eSB0cmVuaW5nb3dlIGJ5xYJ5IHphcGlzeXdhbmUgdyBwb3N0YWNpIG1hY2llcnp5IG8gd3ltaWFyYWNoICg2MDAwMCwgMjgsIDI4KSwgemF3aWVyYWrEhWNlaiB3YXJ0b8WbY2kgeiB6YWtyZXN1IFswLCAyNTVdLCBpIHR5cGllIHVpbnQ4LiBQcnpla3N6dGHFgmNhbXkgamUgdyB0YWJsaWPEmSB0eXB1IGZsb2F0MzIgbyB3eW1pYXJhY2ggKDYwMDAwLCAyOCAqIDI4KSwgemF3aWVyYWrEhWPEhSB3YXJ0b8WbY2kgb2QgMCBkbyAxLg0KDQpgYGB7cn0NCnRyYWluX2ltYWdlcyA8LSBhcnJheV9yZXNoYXBlKHRyYWluX2ltYWdlcywgYyg2MDAwMCwgMjggKiAyOCkpDQp0cmFpbl9pbWFnZXMgPC0gdHJhaW5faW1hZ2VzIC8gMjU1DQoNCnRlc3RfaW1hZ2VzIDwtIGFycmF5X3Jlc2hhcGUodGVzdF9pbWFnZXMsIGMoMTAwMDAsIDI4ICogMjgpKQ0KdGVzdF9pbWFnZXMgPC0gdGVzdF9pbWFnZXMgLyAyNTUNCmBgYA0KDQpNdXNpbXkgZG9kYXRrb3dvIHpha29kb3dhxIcgZXR5a2lldHkgemEgcG9tb2PEhSBrYXRlZ29yaWkgKHByb2NlcyB0ZW4gd3lqYcWbbmnEmSB3IHJvemR6aWFsZSAzLik6DQoNCmBgYHtyfQ0KdHJhaW5fbGFiZWxzIDwtIHRvX2NhdGVnb3JpY2FsKHRyYWluX2xhYmVscykNCnRlc3RfbGFiZWxzIDwtIHRvX2NhdGVnb3JpY2FsKHRlc3RfbGFiZWxzKQ0KYGBgDQoNCkplc3RlxZtteSBnb3Rvd2kgZG8gdXJ1Y2hvbWllbmlhIHRyZW5vd2FuaWEgc2llY2kuIEtvcnp5c3RhbXkgeiBwYWtpZXR1IEtlcmFzLCBhIHdpxJljIG9kd2/FgmFteSBzacSZIGRvIG1ldG9keSBmaXQgbW9kZWx1IChkb3Bhc3VqZW15IG1vZGVsIGRvIGRhbnljaCB0cmVuaW5nb3d5Y2gpOg0KDQpgYGB7ciwgZWNobz1UUlVFLCByZXN1bHRzPSdoaWRlJ30NCm5ldHdvcmsgJT4lIGZpdCh0cmFpbl9pbWFnZXMsIHRyYWluX2xhYmVscywgZXBvY2hzID0gNSwgYmF0Y2hfc2l6ZSA9IDEyOCkNCmBgYA0KDQpQb2RjemFzIHRyZW5vd2FuaWEgd3nFm3dpZXRsYW5lIHPEhSBkd2llIHdhcnRvxZtjaTogc3RyYXRhIHNpZWNpIGkgamVqIGRva8WCYWRub8WbxIcgKG9iaWUgd2FydG/Fm2NpIGRvdHljesSFIHRyZW5pbmdvd2VnbyB6YmlvcnUgZGFueWNoKS4NCg0KVyBjemFzaWUgdHJlbm93YW5pYSBzenlia28gb3NpxIVnYW15IGRva8WCYWRub8WbxIcgMCw5ODkgKDk4LDklKS4gVGVyYXogbW/FvGVteSBzcHJhd2R6acSHIGRva8WCYWRub8WbxIcgcHJ6ZXR3YXJ6YW5pYSB0ZXN0b3dlZ28gemJpb3J1IGRhbnljaDoNCg0KYGBge3J9DQptZXRyaWNzIDwtIG5ldHdvcmsgJT4lIGV2YWx1YXRlKHRlc3RfaW1hZ2VzLCB0ZXN0X2xhYmVscywgdmVyYm9zZSA9IDApDQptZXRyaWNzDQpgYGANCg0KVyBwcnp5cGFka3UgdGVzdG93ZWdvIHpiaW9ydSBkYW55Y2ggdXp5c2thbGnFm215IGRva8WCYWRub8WbxIcgbmEgcG96aW9taWUgOTcsOCUsIGEgd2nEmWMgd2FydG/Fm8SHIG5pZWNvIG5pxbxzesSFIG5pxbwgZGxhIHpiaW9ydSB0cmVuaW5nb3dlZ28uIFLDs8W8bmljYSBtacSZZHp5IHR5bWkgd2FydG/Fm2NpYW1pIHd5bmlrYSB6IG5hZG1pZXJuZWdvIGRvcGFzb3dhbmlhLiBNb2RlbGUgdWN6ZW5pYSBtYXN6eW5vd2VnbyBtYWrEhSB0ZW5kZW5jasSZIGRvIG5pxbxzemVqIGRva8WCYWRub8WbY2kgcHJ6ZXR3YXJ6YW5pYSBub3d5Y2ggZGFueWNoLCBuacW8IHRvIG1pYcWCbyBtaWVqc2NlIHcgcHJ6eXBhZGt1IGRhbnljaCB0cmVuaW5nb3d5Y2guIFphZ2FkbmllbmllIHRvIGplc3QgZ8WCw7N3bnltIHRlbWF0ZW0gcm96ZHppYcWCdSAzLg0KDQpUbyB0eWxlLCBqZcW8ZWxpIGNob2R6aSBvIG5hc3ogcGllcndzenkgcHJ6eWvFgmFkIOKAlCB3xYJhxZtuaWUgem9iYWN6ecWCZcWbLCDFvGUgemJ1ZG93YW5pZSBpIHd5dHJlbm93YW5pZSBzaWVjaSBuZXVyb25vd2VqIGtsYXN5ZmlrdWrEhWNlaiB6YXBpcyByxJljem55IGN5ZnIgbW/FvGUgemFqxIXEhyBtbmllaiBuacW8IDIwIGxpbmlpIGtvZHUgUi4gVyBrb2xlam55bSByb3pkemlhbGUgb3Bpc3rEmSBzemN6ZWfDs8WCb3dvIHdzenlzdGtpZSBsaW5pZSB0ZWdvIGtvZHUgaSB3eWphxZtuacSZLCBqYWtpZSBvcGVyYWNqZSBzxIUgd3lrb255d2FuZSB3IHRsZS4gRG93aWVzeiBzacSZLCBjenltIHPEhSB0ZW5zb3J5LCBvYmlla3R5IHByemV6bmFjem9uZSBkbyBwcnplY2hvd3l3YW5pYSBkYW55Y2ggc2llY2kgaSBvcGVyYWNqZSBuYSB0ZW5zb3JhY2guIFBvem5hc3ogYnVkb3fEmSB3YXJzdHcgc2llY2kgaSBhbGdvcnl0bSBzcGFka3UgZ3JhZGllbnRvd2Vnbywga3TDs3J5IHVtb8W8bGl3aWEgc2llY2kgdWN6ZW5pZSBzacSZIG5hIHBvZHN0YXdpZSB0cmVuaW5nb3dlZ28gemJpb3J1IGRhbnljaC4NCg0K