MNIST 데이터(우편번호)로 classification을 연습해보자.
데이터 살펴보기
- 이번에 분석할 내용은 MNIST라고 부르는 유명한 우편번호 데이터이다.
- 숫자가 적힌 수많은 데이터를 classification시켜서 기계가 스스로 숫자를 읽을 수 있도록 하는 것이다.
- 실제 우체국에서 우편번호를 인식해서 기계가 자동으로 우편물을 분류하도록 이용하고 있다.
변수개수 & 관측치 개수
dim(mnist.train) ; dim(mnist.test) ;range(mnist.train[,-1])
[1] 60000 785
[1] 10000 785
[1] 0 1
- 각각 785개의 변수로 구성되었다.
- 각 변수는 이미지의 픽셀을 의미하며, 변수값은 픽셀의 명도를 나타낸다.
- 픽셀의 명도는 0~1 사이의 값을 가진다.
- training set은 6만건의 데이터로 이루어져있고, - test set은 만건의 데이터로 이루어져있다.
결측값 확인
sum(is.na(mnist.train)) ;sum(is.na(mnist.test))
[1] 0
[1] 0
- 결측값은 하나도 없다. 그럼 기분좋게 분석을 시작해보도록 하자!
Knn (with LOOCV)
- Knn classification이란, 해당 데이터와 거리가 가장 가까운 데이터들을 참고해서 분류분석하는 방법이다.
- 예림이의 성적을 알고 싶다면, 예림이의 친한 친구들의 성적을 참고해서, 예림이의 성적을 유추해보는 것과 같은 알고리즘이다. - Knn을 LOOCV를 이용해서 분석해보았다.
error.list
[1] 0.0309 0.0309 0.0309 0.0309 0.0309 0.0309 0.0309 0.0309 0.0309 0.0309
- k=1,2,```,10의 결과가 모두 동일하다. error rate(0.0309)가 모두 같다.
mean(predict.knn[[1]]== as.factor(mnist.test[,1])) #accuracy rate
[1] 0.9691
- knn의 accuracy rate는 0.9691로 나왔다.
table(predict.knn[[1]], as.factor(mnist.test[,1]))
0 1 2 3 4 5 6 7 8 9
0 973 0 7 0 0 1 4 0 6 2
1 1 1129 6 1 7 1 2 14 1 5
2 1 3 992 2 0 0 0 6 3 1
3 0 0 5 970 0 12 0 2 14 6
4 0 1 1 1 944 2 3 4 5 10
5 1 1 0 19 0 860 5 0 13 5
6 3 1 2 0 3 5 944 0 3 1
7 1 0 16 7 5 1 0 992 4 11
8 0 0 3 7 1 6 0 0 920 1
9 0 0 0 3 22 4 0 10 5 967
- k=1인 경우로 놓고 구한 confustion matrix이다.
- 특별한 규칙성이나 경향은 보이지 않는다.
- 실제값은 4인데, 9로 misclassify한 경우가 22건으로 가장 많고,
- 두번째로 많은 misclassification은 실제값이 3인것을 5로 misclassify한 경우이다.
랜덤포레스트로 넘어가보자.
randomforest
rf.h
Call:
randomForest(formula = as.factor(y) ~ ., data = mnist.train, mtry = 28, importance = T)
Type of random forest: classification
Number of trees: 500
No. of variables tried at each split: 28
OOB estimate of error rate: 2.96%
Confusion matrix:
0 1 2 3 4 5 6 7 8 9 class.error
0 5852 1 6 2 5 4 16 2 31 4 0.01198717
1 1 6648 33 9 12 4 5 12 12 6 0.01394245
2 24 10 5795 17 19 2 17 31 36 7 0.02735817
3 7 5 74 5868 1 43 7 45 53 28 0.04289675
4 9 10 11 0 5679 0 22 11 12 88 0.02790140
5 18 6 10 55 8 5227 42 4 32 19 0.03578676
6 20 8 3 0 10 28 5831 0 18 0 0.01470091
7 6 20 50 4 36 1 0 6069 12 67 0.03128492
8 10 28 31 42 25 32 28 4 5589 62 0.04477867
9 22 10 10 72 61 17 4 45 40 5668 0.04723483
- 랜덤포레스트 결과이다.
- split은 \(\sqrt 785 = 28\)로 놓고 풀었다.
- OOB error rate는 약 2.96%이다. 결과가 정말 좋다. 오류가 이렇게나 적다니…!
- 각 숫자의 class error rate를 살펴보면 3, 8, 9의 error rate가 각각 0.042, 0.044, 0.047로 비교적 높게 나왔다.
- 3,8,9 -> 이 세 숫자 모양이 비슷해서 그런 것 같다.
- 실제로 confusion matrix를 통해 misclassification한 것을 보면 3과 8,9를 서로 혼동해서 classification 한 경우가 많다는 것을 볼 수 있다.
- knn에서는 오분류에대한 어떤 특정한 경향이 안 보였는데,
오류를 줄이고 싶다면, 3,8,9를 잘 구분할 수 있도록 학습 시키는 것이 중요하겠다.
mean(yhat.rf!=mnist.test$y); mean(yhat.rf==mnist.test$y)
[1] 0.0288
[1] 0.9712
- randomforest로 분석했을 때의 error rate는 0.0288이다.
- accuracy rate는 0.9712이다.
variance importance plot

# 가장 많은 영향을 주는 변수 상위 10개를 골랐다.
head(order(tmp$MeanDecreaseAccuracy, decreasing=T), n=10)
[1] 294 297 322 325 321 295 298 269 349 575
head(order(tmp$MeanDecreaseGini, decreasing=T), n=10)
[1] 379 407 351 410 378 212 462 406 434 156
- 어떤 변수가 중요한 영향을 미치는지 살펴보았는데, 현 데이터에서는 큰 의미가 없는 것 같다.
- 이미지 데이터에서는 변수가 중요한 의미를 가지지 않기 때문이다…..
마지막으로 boosting을 해보자.
Boosting
result.boosting
Call:
maboost(mnist.train[, -1], y = as.factor(mnist.train$y))
Loss: Method: normal Iteration: 100
Final Confusion Matrix for Data:
g
0 1 2 3 4 5 6 7 8 9
0 5515 0 26 20 13 147 52 7 128 15
1 1 6446 57 30 5 26 10 13 129 25
2 44 65 5121 97 123 54 104 114 205 31
3 28 56 179 5150 18 267 27 57 211 138
4 16 15 32 11 5158 6 59 17 64 464
5 52 49 50 301 90 4544 76 22 112 125
6 58 38 127 6 112 134 5376 7 58 2
7 14 81 91 6 114 12 0 5497 60 390
8 15 93 59 260 47 135 43 14 4988 197
9 36 19 40 97 239 26 6 141 102 5243
Train Error: 0.116
Out-Of-Bag Error: 0.127 iteration= 100
Additional Estimates of number of iterations:
train.err1 train.kap1
99 99
mean(result.boosting$fit == mnist.train$y) #accuracy
[1] 0.8839667
- boosting 결과가 생각보다 좋게 나오지 않았다.
- accaracy rate는 0.8840이고, error rate는 0.1160.
- OOB error는 0.127로 나왔다(test error의 추정값이라 생각하면 된다)
- boosting은 원래 binary output에 최적화된건데, 우리 데이터는 multiclass output이어서 모형적합도가 떨어지는 것 같다.
Summary
| |
Acc |
error |
ET |
Remarks |
| Knn |
0.97 |
0.031 |
19h |
계산시간 너무 오래 걸림 |
| RF |
0.97 |
0.029 |
6h |
boosting보다 짧은 계신시간에 비슷한 정확도 |
| Boosting |
0.88 |
0.116 |
2h |
계산시간 매우 짧으나, 정확도가 너무 낮아서 아쉬움 |
Warning message:
In strsplit(code, "\n", fixed = TRUE) :
input string 1 is invalid in this locale
- knn, randomforest, boosting 이상 3개 방법을 종합해보았다.
- 나라면 조금 오래 걸리더라도 randomforest로 분석할 것 같다.
끝!!!
LS0tDQp0aXRsZTogIkRhdGEgbWluaW5nIEhXNCAoUmV2aXNlZCB2ZXIuKSINCkF1dGhvcjogIlllcmltIExpbSINCkRhdGU6ICIyMDE464WEIDbsm5QgMuydvCINCm91dHB1dDogDQogIGh0bWxfbm90ZWJvb2s6DQogICAgdG9jOiB0cnVlDQotLS0NCg0KPiBNTklTVCDrjbDsnbTthLAo7Jqw7Y6467KI7Zi4KeuhnCBjbGFzc2lmaWNhdGlvbuydhCDsl7DsirXtlbTrs7TsnpAuICANCg0KIyDrjbDsnbTthLAg7IK07Y6067O06riwIA0KYGBge3IgaW5jbHVkZT1GQUxTRSwgZWNobz1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChjb21tZW50ID0gIiIgLCBwcm9tcHQgPSBUUlVFLCBvdXQud2lkdGggPSA1MDAsIGZpZy5oZWlnaHQgPSAxMCwgZmlnLndpZHRoID0gMTApDQpsaWJyYXJ5KGtuaXRyKQ0KU3lzLnNldGxvY2FsZSgiTENfQUxMIiwgIktvcmVhbiIpDQpzZXR3ZCgiRTovRHJvcGJveC8wMC4yMDE4LzAxLjIwMThfMV9zZW1lc3Rlci8wMS5EYXRhTWluaW5nLzA0LkhXL0hXNCIpDQpgYGAgDQoNCi0g7J2067KI7JeQIOu2hOyEne2VoCDrgrTsmqnsnYAgTU5JU1Trnbzqs6Ag67aA66W064qUIOycoOuqhe2VnCDsmrDtjrjrsojtmLgg642w7J207YSw7J2064ukLiANCi0g7Iir7J6Q6rCAIOygge2ejCDsiJjrp47snYAg642w7J207YSw66W8IGNsYXNzaWZpY2F0aW9u7Iuc7Lyc7IScIOq4sOqzhOqwgCDsiqTsiqTroZwg7Iir7J6Q66W8IOydveydhCDsiJgg7J6I64+E66GdIO2VmOuKlCDqsoPsnbTri6QuIA0KLSDsi6TsoJwg7Jqw7LK06rWt7JeQ7IScIOyasO2OuOuyiO2YuOulvCDsnbjsi53tlbTshJwg6riw6rOE6rCAIOyekOuPmeycvOuhnCDsmrDtjrjrrLzsnYQg67aE66WY7ZWY64+E66GdIOydtOyaqe2VmOqzoCDsnojri6QuIA0KDQoNCmBgYHtyIGVjaG89RkFMU0V9DQptbmlzdC50cmFpbi5sYXJnZSA8LSByZWFkLmNzdigiTU5JU1RfdHJhaW4uY3N2IikNCm1uaXN0LnRlc3QubGFyZ2UgPC0gcmVhZC5jc3YoIk1OSVNUX3Rlc3QuY3N2IikNCg0KbW5pc3QudHJhaW4gPC0gbW5pc3QudHJhaW4ubGFyZ2UNCm1uaXN0LnRlc3Q8LSBtbmlzdC50ZXN0LmxhcmdlDQoNCmBgYA0KDQojIyMjIOuzgOyImOqwnOyImCAmIOq0gOy4oey5mCDqsJzsiJgNCmBgYHtyfQ0KZGltKG1uaXN0LnRyYWluKSA7IGRpbShtbmlzdC50ZXN0KSA7cmFuZ2UobW5pc3QudHJhaW5bLC0xXSkNCg0KYGBgDQotIOqwgeqwgSA3ODXqsJzsnZgg67OA7IiY66GcIOq1rOyEseuQmOyXiOuLpC4gDQotIOqwgSDrs4DsiJjripQg7J2066+47KeA7J2YIO2UveyFgOydhCDsnZjrr7jtlZjrqbAsIOuzgOyImOqwkuydgCDtlL3shYDsnZgg66qF64+E66W8IOuCmO2DgOuCuOuLpC4gDQotIO2UveyFgOydmCDrqoXrj4TripQgMH4xIOyCrOydtOydmCDqsJLsnYQg6rCA7KeE64ukLiANCg0KPGJyLz4NCi0gdHJhaW5pbmcgc2V07J2AIDbrp4zqsbTsnZgg642w7J207YSw66GcIOydtOujqOyWtOyguOyeiOqzoCwNCi0gdGVzdCBzZXTsnYAg66eM6rG07J2YIOuNsOydtO2EsOuhnCDsnbTro6jslrTsoLjsnojri6QuIA0KDQoNCg0KIyMjIyDqsrDsuKHqsJIg7ZmV7J24DQpgYGB7cn0NCnN1bShpcy5uYShtbmlzdC50cmFpbikpIDtzdW0oaXMubmEobW5pc3QudGVzdCkpDQpgYGANCi0g6rKw7Lih6rCS7J2AIO2VmOuCmOuPhCDsl4bri6QuIOq3uOufvCDquLDrtoTsoovqsowg67aE7ISd7J2EIOyLnOyeke2VtOuztOuPhOuhnSDtlZjsnpAhDQoNCg0KDQoNCg0KIyBLbm4gKHdpdGggTE9PQ1YpDQotIEtubiBjbGFzc2lmaWNhdGlvbuydtOuegCwg7ZW064u5IOuNsOydtO2EsOyZgCDqsbDrpqzqsIAg6rCA7J6lIOqwgOq5jOyatCDrjbDsnbTthLDrk6TsnYQg7LC46rOg7ZW07IScIOu2hOulmOu2hOyEne2VmOuKlCDrsKnrspXsnbTri6QuIA0KLSDsmIjrprzsnbTsnZgg7ISx7KCB7J2EIOyVjOqzoCDsi7bri6TrqbQsIOyYiOumvOydtOydmCDsuZztlZwg7Lmc6rWs65Ok7J2YIOyEseyggeydhCDssLjqs6DtlbTshJwsIOyYiOumvOydtOydmCDshLHsoIHsnYQg7Jyg7LaU7ZW067O064qUIOqyg+qzvCDqsJnsnYAg7JWM6rOg66as7KaY7J2064ukLiAtIEtubuydhCBMT09DVuulvCDsnbTsmqntlbTshJwg67aE7ISd7ZW067O07JWY64ukLiANCg0KYGBge3IsIGVjaG89RkFMU0V9DQpsaWJyYXJ5KGNsYXNzKQ0Kc2V0LnNlZWQoMTIzNCkNCmVycm9yPC1saXN0KCkNCnByZWRpY3Qua25uIDwtIGxpc3QoKQ0KDQpmb3IgKGkgaW4gMToxMCl7DQogIHByZWRpY3Qua25uW1tpXV0gPC0ga25uLmN2KG1uaXN0LnRyYWluWywtMV0sDQogICAgICAgICAgICAgICAgICAgbW5pc3QudHJhaW5bLDFdLGs9MSkNCn0NCmBgYA0KDQoNCmBgYHtyfQ0KZXJyb3IgPC0gbGlzdCgpDQpmb3IgKCBpIGluIDE6MTApew0KZXJyb3JbW2ldXTwtbWVhbihwcmVkaWN0LmtubltbaV1dIT1tbmlzdC50ZXN0WywxXSkNCn0NCg0KZXJyb3IubGlzdCA8LSB1bmxpc3QoZXJyb3IpDQplcnJvci5saXN0DQpgYGANCg0KLSBrPTEsMixgYGAsMTDsnZgg6rKw6rO86rCAIOuqqOuRkCDrj5nsnbztlZjri6QuIGVycm9yIHJhdGUoMC4wMzA5KeqwgCDrqqjrkZAg6rCZ64ukLiANCmBgYHtyfQ0KbWVhbihwcmVkaWN0LmtubltbMV1dPT0gYXMuZmFjdG9yKG1uaXN0LnRlc3RbLDFdKSkgI2FjY3VyYWN5IHJhdGUNCg0KYGBgDQotIGtubuydmCBhY2N1cmFjeSByYXRl64qUIDAuOTY5MeuhnCDrgpjsmZTri6QuIA0KDQpgYGB7cn0NCg0KdGFibGUocHJlZGljdC5rbm5bWzFdXSwgYXMuZmFjdG9yKG1uaXN0LnRlc3RbLDFdKSkNCg0KYGBgDQoNCi0gaz0x7J24IOqyveyasOuhnCDrhpPqs6Ag6rWs7ZWcIGNvbmZ1c3Rpb24gbWF0cml47J2064ukLiANCi0g7Yq567OE7ZWcIOq3nOy5meyEseydtOuCmCDqsr3tlqXsnYAg67O07J207KeAIOyViuuKlOuLpC4gDQotIOyLpOygnOqwkuydgCA07J24642wLCA566GcIG1pc2NsYXNzaWZ57ZWcIOqyveyasOqwgCAyMuqxtOycvOuhnCDqsIDsnqUg66eO6rOgLCANCi0g65GQ67KI7Ke466GcIOunjuydgCBtaXNjbGFzc2lmaWNhdGlvbuydgCDsi6TsoJzqsJLsnbQgM+yduOqyg+ydhCA166GcIG1pc2NsYXNzaWZ57ZWcIOqyveyasOydtOuLpC4gDQoNCj4g656c642k7Y+s66CI7Iqk7Yq466GcIOuEmOyWtOqwgOuztOyekC4gDQoNCioqKg0KDQoNCg0KIyByYW5kb21mb3Jlc3QNCmBgYHtyfQ0KbGlicmFyeShyYW5kb21Gb3Jlc3QpDQpzZXQuc2VlZCgxKQ0KcmYuaDwtcmFuZG9tRm9yZXN0KGFzLmZhY3Rvcih5KX4uLGRhdGE9bW5pc3QudHJhaW4sbXRyeT0yOCxpbXBvcnRhbmNlPVQpDQpyZi5oDQoNCg0KYGBgDQotIOuenOuNpO2PrOugiOyKpO2KuCDqsrDqs7zsnbTri6QuIA0KLSBzcGxpdOydgCAkXHNxcnQgNzg1ID0gMjgk66GcIOuGk+qzoCDtkoDsl4jri6QuIA0KLSBPT0IgZXJyb3IgcmF0ZeuKlCDslb0gMi45NiXsnbTri6QuIOqysOqzvOqwgCDsoJXrp5Ag7KKL64ukLiDsmKTrpZjqsIAg7J2066CH6rKM64KYIOyggeuLpOuLiC4uLiENCi0g6rCBIOyIq+yekOydmCBjbGFzcyBlcnJvciByYXRl66W8IOyCtO2OtOuztOuptCAqKjMqKiwgKio4KiosICoqOSoq7J2YIGVycm9yIHJhdGXqsIAg6rCB6rCBIDAuMDQyLCAwLjA0NCwgMC4wNDfroZwg67mE6rWQ7KCBIOuGkuqyjCDrgpjsmZTri6QuDQotIDMsOCw5IC0+IOydtCDshLgg7Iir7J6QIOuqqOyWkeydtCDruYTsirftlbTshJwg6re465+wIOqygyDqsJnri6QuIA0KLSDsi6TsoJzroZwgY29uZnVzaW9uIG1hdHJpeOulvCDthrXtlbQgbWlzY2xhc3NpZmljYXRpb27tlZwg6rKD7J2EIOuztOuptCAgM+qzvCA4LDnrpbwg7ISc66GcIO2YvOuPme2VtOyEnCBjbGFzc2lmaWNhdGlvbiDtlZwg6rK97Jqw6rCAIOunjuuLpOuKlCDqsoPsnYQg67O8IOyImCDsnojri6QuIA0KLSBrbm7sl5DshJzripQg7Jik67aE66WY7JeQ64yA7ZWcIOyWtOuWpCDtirnsoJXtlZwg6rK97Zal7J20IOyViCDrs7TsmIDripTrjbAsIA0KDQo+IOyYpOulmOulvCDspITsnbTqs6Ag7Iu264uk66m0LCAzLDgsOeulvCDsnpgg6rWs67aE7ZWgIOyImCDsnojrj4TroZ0g7ZWZ7Iq1IOyLnO2CpOuKlCDqsoPsnbQg7KSR7JqU7ZWY6rKg64ukLiANCg0KDQpgYGB7ciBpbmNsdWRlPUZBTFNFfQ0KeWhhdC5yZjwtcHJlZGljdChyZi5oLG5ld2RhdGE9bW5pc3QudGVzdCkNCg0KYGBgDQoNCmBgYHtyfQ0KbWVhbih5aGF0LnJmIT1tbmlzdC50ZXN0JHkpOyBtZWFuKHloYXQucmY9PW1uaXN0LnRlc3QkeSkNCmBgYA0KLSByYW5kb21mb3Jlc3TroZwg67aE7ISd7ZaI7J2EIOuVjOydmCBlcnJvciByYXRl64qUIDAuMDI4OOydtOuLpC4NCi0gYWNjdXJhY3kgcmF0ZeuKlCAwLjk3MTLsnbTri6QuICANCg0KPGJyLz4NCg0KIyMjIyB2YXJpYW5jZSBpbXBvcnRhbmNlIHBsb3QNCmBgYHtyLCBmaWcud2lkdGg9MTUsIGZpZy5oZWlnaHQ9MTV9DQojIGltcG9ydGFuY2UocmYuaCkNCnZhckltcFBsb3QocmYuaCkNCmBgYA0KDQoNCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0V9DQp0bXAgPC0gaW1wb3J0YW5jZShyZi5oKQ0KdG1wIDwtIGFzLmRhdGEuZnJhbWUodG1wKQ0KDQojIG1lYW4gZGVjcmVhc2UgYWNjdXJhY3nrpbwg7J207Jqp7ZW0IOqwgOyepSDrp47snYAg7JiB7Zal7J2EIOyjvOuKlCDrs4DsiJgg7IOB7JyEIDEw6rCc66W8IOqzqOuekOuLpC4gDQpoZWFkKG9yZGVyKHRtcCRNZWFuRGVjcmVhc2VBY2N1cmFjeSwgZGVjcmVhc2luZz1UKSwgbj0xMCkNCmBgYA0KDQpgYGB7cn0NCiMgbWVhbiBkZWNyZWFzZSBnaW5p66W8IO2Gte2VtCDsgrTtjrTrs7gg7IOB7JyEIDEw6rCcIOuzgOyImEBAQA0KaGVhZChvcmRlcih0bXAkTWVhbkRlY3JlYXNlR2luaSwgZGVjcmVhc2luZz1UKSwgbj0xMCkNCmBgYA0KLSDslrTrlqQg67OA7IiY6rCAIOykkeyalO2VnCDsmIHtlqXsnYQg66+47LmY64qU7KeAIOyCtO2OtOuztOyVmOuKlOuNsCwg7ZiEIOuNsOydtO2EsOyXkOyEnOuKlCDtgbAg7J2Y66+46rCAIOyXhuuKlCDqsoMg6rCZ64ukLiANCi0g7J2066+47KeAIOuNsOydtO2EsOyXkOyEnOuKlCDrs4DsiJjqsIAg7KSR7JqU7ZWcIOydmOuvuOulvCDqsIDsp4Dsp4Ag7JWK6riwIOuVjOusuOydtOuLpC4uLi4uIA0KDQo+IOuniOyngOunieycvOuhnCBib29zdGluZ+ydhCDtlbTrs7TsnpAuIA0KDQoqKioNCg0KIyBCb29zdGluZw0KYGBge3IgaW5jbHVkZT1GQUxTRX0NCmxpYnJhcnkobWFib29zdCkNCnJlc3VsdC5ib29zdGluZyA8LSBtYWJvb3N0KG1uaXN0LnRyYWluWywtMV0sIA0KICAgICAgICAgICAgICAgICAgYXMuZmFjdG9yKG1uaXN0LnRyYWluJHkpKQ0KDQpgYGANCg0KYGBge3J9DQpyZXN1bHQuYm9vc3RpbmcNCm1lYW4ocmVzdWx0LmJvb3N0aW5nJGZpdCA9PSBtbmlzdC50cmFpbiR5KSAjYWNjdXJhY3kNCg0KYGBgDQotIGJvb3N0aW5nIOqysOqzvOqwgCDsg53qsIHrs7Tri6Qg7KKL6rKMIOuCmOyYpOyngCDslYrslZjri6QuIA0KLSBhY2NhcmFjeSByYXRl64qUIDAuODg0MOydtOqzoCwgZXJyb3IgcmF0ZeuKlCAwLjExNjAuDQotIE9PQiBlcnJvcuuKlCAwLjEyN+uhnCDrgpjsmZTri6QodGVzdCBlcnJvcuydmCDstpTsoJXqsJLsnbTrnbwg7IOd6rCB7ZWY66m0IOuQnOuLpCkNCi0gYm9vc3RpbmfsnYAg7JuQ656YIGJpbmFyeSBvdXRwdXTsl5Ag7LWc7KCB7ZmU65Cc6rG0642wLCDsmrDrpqwg642w7J207YSw64qUIG11bHRpY2xhc3Mgb3V0cHV07J207Ja07IScIOuqqO2Yleygge2VqeuPhOqwgCDrlqjslrTsp4DripQg6rKDIOqwmeuLpC4gDQoNCioqKg0KDQoNCg0KIyBTdW1tYXJ5DQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCBlY2hvPUZBTFNFfQ0Kb3B0aW9ucyhrbml0ci50YWJsZS5mb3JtYXQ9Imh0bWwiKQ0Kb3B0aW9ucyhrYWJsZUV4dHJhLmF1dG9fZm9ybWF0PUYpDQoNCmxpYnJhcnkoa2FibGVFeHRyYSkNCm5hbWVzIDwtIGMoIktubiIsIlJGIiwiQm9vc3RpbmciKQ0KQWNjIDwtIGMoMC45NjkxLCAwLjk3LDAuODgpDQplcnJvciA8LSBjKDAuMDMxLCAwLjAyOSwgMC4xMTYpDQpFVCA8LSBjKCIxOWgiLCI2aCIsIjJoIikNClJlbWFya3MgPC0gYygi6rOE7IKw7Iuc6rCEIOuEiOustCDsmKTrnpgg6rG466a8IiwNCiAgICAgICAgICAgICAiYm9vc3Rpbmfrs7Tri6Qg7Ken7J2AIOqzhOyLoOyLnOqwhOyXkCDruYTsirftlZwg7KCV7ZmV64+EIiwNCiAgICAgICAgICAgICAi6rOE7IKw7Iuc6rCEIOunpOyasCDsp6fsnLzrgpgsIOygle2ZleuPhOqwgCDrhIjrrLQg64Ku7JWE7IScIOyVhOyJrOybgCAiKQ0KbmFtZXMoQWNjKSA8LSBuYW1lcw0KbmFtZXMoZXJyb3IpIDwtIG5hbWVzDQpuYW1lcyhFVCkgPC0gbmFtZXMNCm5hbWVzKFJlbWFya3MpIDwtIG5hbWVzDQpkZiA8LSBhcy5kYXRhLmZyYW1lKGNiaW5kKEFjYyxlcnJvciwgRVQsIFJlbWFya3MpKQ0KDQoNCmthYmxlKGRmLCByb3cubmFtZXMgPSBULCBmb3JtYXQ9Imh0bWwiKSU+JSANCiAga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9IGMoInN0cmlwZWQiLCJob3ZlciIpLGZ1bGxfd2lkdGg9RikgJT4lIA0KICBjb2x1bW5fc3BlYyg0LCB3aWR0aCA9ICIzZW0iICkNCmBgYA0KDQotIGtubiwgcmFuZG9tZm9yZXN0LCBib29zdGluZyDsnbTsg4EgM+qwnCDrsKnrspXsnYQg7KKF7ZWp7ZW067O07JWY64ukLiANCi0g64KY652866m0IOyhsOq4iCDsmKTrnpgg6rG466as642U652864+EIHJhbmRvbWZvcmVzdOuhnCDrtoTshJ3tlaAg6rKDIOqwmeuLpC4gDQoNCj4g64GdISEh