작성 중

# 데이터 분석은 아래와 같은 순서대로 중요하지 않을까 하는 2017년 현재 생각.
# 30% 도메인 이해
# 30% EDA + FE 
# 15% 데이터 정제 
# 10% 평가        
# 10% 모델링      
# 5%  앙상블       
#
# 이중 개별 모델링을 제외한 나머지를 알아야, 와꾸가 서는 것이고
# 개별 모델링 기법만 몇개 아는 것은 ... 생각보다 덜 중요할 수 도 있다는 생각. 
# 특히 도메인 이해는 시간과 애정을 많이 쏟아야 하는 부분 같고, 올바른 평가에도 많은 시간을 써야 하는 것 같다. ( 이 부분은 보통 간과되기 슆다. )

terms

# Bagging
# Boosting
# Stacked Generalization
# 
# XGBOOST
# LASAGNE NN
# ADABOOST ET
#
# Stacking
# Generalization Error
# meta features / out-of-fold predictions
# meta learning
#
# Average
# Less Variance
# Less Generalization Error
# Less chance of overfitting

meta learning

# 1. meta learning?
#  - automatic learning algorithms are applied on metadata about machine learning experiments 
#
# 2. variations of meta learning
#  * selection ( algorithm learning )
#  * hyper-parameter optimization
#  * ensemble ( bagging . boosting . stacked generalization )

ensemble

ensemble method > bagging ( = Bootstrap Aggregating )

# Bagging generates a number of training datasets by bootstrap sampling the original training data. 
# These datasets are then used to generate a set of models using a single learning algorithm. 
# The models' predictions are combined using voting (for classi cation) or averaging (for numeric prediction).
#
# 1. BootStrap : training dataset으로부터 크기가 같은 표본을 반복추출 (bootstrap sampling)
# 2. Generating Model : 각각에 대해 하나의 ML 알고리즘에 따라 모델 생성, 
# 3. Ensemble : 각 모델들의 결과를 종합하여 의사결정을 내리는 방법( voting / average )
#
# it can perform quite well as long as it is used with relatively unstable learners,
# that is, those generating models that tend to change substantially when the input data changes only slightly
#
# bagging is often used with decision trees, which have the tendency to vary dramatically given minor changes in the input data.
#
# variance 를 줄이는 효과.
library(ipred)
credit <- read.csv("/Users/CA/Machine-Learning-with-R-datasets/credit.csv")
set.seed(1234)
rf.bag <- bagging(default ~ ., data = credit, nbagg = 25)
credit$pred <- predict(rf.bag, credit)
library(InformationValue)
threshold <- optimalCutoff(credit$default, credit$pred)
confusionMatrix(credit$default, credit$pred, threshold = threshold)
library(caret)
Loading required package: lattice
Loading required package: ggplot2

Attaching package: 'ggplot2'

The following object is masked from 'package:randomForest':

    margin


Attaching package: 'caret'

The following objects are masked from 'package:InformationValue':

    confusionMatrix, precision, sensitivity, specificity
credit <- read.csv("/Users/CA/Machine-Learning-with-R-datasets/credit.csv")
credit <- transform(credit, default = ifelse(default == 1, "no", "yes"))
ctrl <- trainControl(method = "cv", number = 10)
train(default ~ ., data = credit, method = "treebag", trControl = ctrl)
Loading required package: plyr
Loading required package: e1071
Bagged CART 

1000 samples
  20 predictor
   2 classes: 'no', 'yes' 

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 900, 900, 900, 900, 900, 900, ... 
Resampling results:

  Accuracy  Kappa    
  0.752     0.3742893
# The caret package also includes example objects for bags of 
# naive Bayes models (nbBag), decision trees (ctreeBag), and neural networks (nnetBag).
str(svmBag)
List of 3
 $ fit      :function (x, y, ...)  
 $ pred     :function (object, x)  
 $ aggregate:function (x, type = "class")  
library(caret)
library(kernlab)
library(e1071)

credit <- read.csv("/Users/CA/Machine-Learning-with-R-datasets/credit.csv")
credit <- transform(credit, default = ifelse(default == 1, "no", "yes"))

svm.predict <- function (object, x)
{
 if (is.character(lev(object))) {
    out <- predict(object, as.matrix(x), type = "probabilities")
    colnames(out) <- lev(object)
    rownames(out) <- NULL
  }
  else out <- predict(object, as.matrix(x))[, 1]
  out
}

bagCtrl <- bagControl(fit = svmBag$fit, predict = svm.predict, aggregate = svmBag$aggregate)
trCtrl    <- trainControl(method = "cv", number = 10)

svmbag.result <- train(default ~., data = credit, method = "bag", trControl = trCtrl, bagControl = bagCtrl, verbose = F)
svmbag.result
Bagged Model 

1000 samples
  20 predictor
   2 classes: 'no', 'yes' 

No pre-processing
Resampling: Cross-Validated (10 fold) 
Summary of sample sizes: 900, 900, 900, 900, 900, 900, ... 
Resampling results:

  Accuracy  Kappa    
  0.764     0.4015154

Tuning parameter 'vars' was held constant at a value of 48

ensemble method > boosting

# it boosts the performance of weak learners to attain the performance of stronger learners
# boosting uses ensembles of models trained on resampled data and a vote to determine the  nal prediction
#
# First, the resampled datasets in boosting are constructed specically to generate complementary learners. 
# Second, rather than giving each learner an equal vote, boosting gives each learner's vote a weight based on its past performance.
# 
# variance 와 함께, bias 도 줄여가는 효과 (weak learner / strong learner 간 weight 조정)
library(randomForest)
library(data.table)
library(gbm)
library(ggplot2)
library(plyr)
library(dplyr)
library(rpart)
x <- seq(-2,2,by=0.01)
lenx<- length(x)
y <- 2 + 3*x^2 + rnorm(lenx, 0, 0.5)
y_r <- 2 + 3*x^2
x.y <- data.frame(x=x,y=y, y_r=y_r)
x.y.samp <- x.y %>% sample_frac(0.5)
x.y.samp.test <- x.y %>% sample_frac(0.1)
mdl_cart <- rpart(y ~ x, data=x.y.samp)
x.y.samp.test$cart_fit <- predict(mdl_cart, newdata=x.y.samp.test)
mdl_rf <- randomForest(y ~ x,data=x.y.samp)
x.y.samp.test$rf_fit <- predict(mdl_rf,newdata=x.y.samp.test)
ggplot(x.y.samp, aes(x,y_r)) + geom_line(size=1.5, colour='black') + geom_point(aes(y=y), size=1) + geom_line(data =  x.y.samp.test, aes(x=x, y=cart_fit), colour = "blue") + geom_line(data =  x.y.samp.test, aes(x=x, y=rf_fit), colour = "red")

shrink <- 0.1
#regression based boosting
y_n <- x.y.samp$y
x   <- x.y.samp$x
v_y_l <- list()
for(i in 1:100){
  lm_fit <- lm(y_n ~ x*I(0 < x))
  v_y <- shrink * predict(lm_fit)
  v_y_l[[i]] <- shrink * predict(lm_fit, newdata=x.y.samp.test)
  resid_n <-  y_n - v_y
  y_n <- resid_n
}
x.y.samp.test$lm_fit   <- apply(as.data.table(v_y_l),1,sum)
x.y.samp.test$lm_fit_3 <- apply(as.data.table(v_y_l)[,1:10,with=F],1,sum)
x.y.samp.test$lm_fit_2 <- apply(as.data.table(v_y_l)[,1:5,with=F],1,sum)
x.y.samp.test$lm_fit_1 <- apply(as.data.table(v_y_l)[,1:2,with=F],1,sum)
ggplot(x.y.samp, aes(x=x,y=y_r)) + geom_line(size=1.5, colour='red') + geom_point(aes(y=y), size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=lm_fit), colour='purple', linetype=2, size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=lm_fit_2),colour='purple',linetype=4) + geom_line(data=x.y.samp.test, aes(x=x,y=lm_fit_3),colour='purple',linetype=4) + geom_line(data=x.y.samp.test,aes(x=x,y=lm_fit_1),colour='purple',linetype=4)

#cart based boosting
y_n <- x.y.samp$y
x <- x.y.samp$x
v_y_l <- list()
for(i in 1:100){
  rpart_fit <- rpart(y_n ~ x)
  v_y <- shrink * predict(rpart_fit)
  v_y_l[[i]] <- shrink * predict(rpart_fit, newdata=x.y.samp.test)
  resid_n <-  y_n - v_y
  y_n <- resid_n
}
x.y.samp.test$rpart_fit <- apply(as.data.table(v_y_l),1,sum)
x.y.samp.test$rpart_fit_3 <- apply(as.data.table(v_y_l)[,1:10,with=F],1,sum)
x.y.samp.test$rpart_fit_2 <- apply(as.data.table(v_y_l)[,1:5,with=F],1,sum)
x.y.samp.test$rpart_fit_1 <- apply(as.data.table(v_y_l)[,1:2,with=F],1,sum)
ggplot(x.y.samp, aes(x=x,y=y_r)) + geom_line(size=1.5, colour='red') + geom_point(aes(y=y), size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=rpart_fit), colour='purple', linetype=2, size=1) + geom_line(data=x.y.samp.test, aes(x=x,y=rpart_fit_2),colour='purple',linetype=4) + geom_line(data=x.y.samp.test, aes(x=x,y=rpart_fit_3),colour='purple',linetype=4) + geom_line(data=x.y.samp.test,aes(x=x,y=rpart_fit_1),colour='purple',linetype=4) 

ensemble method > boosting > adaptive boosting > AdaBoost
# AdaBoost.M1 algorithm : adabag pacakge
library(adabag)
Loading required package: mlbench

Attaching package: 'adabag'

The following object is masked from 'package:ipred':

    bagging
data(iris)
iris.adaboost <- boosting(Species~., data = iris, boos = TRUE, mfinal = 10, coeflearn = "Breiman")
importanceplot(iris.adaboost)

iris.adaboost$weights
 [1] 1.4381928 1.6711499 1.2659370 0.8562498 0.8457676 1.4800669 0.7712694 0.9158454 1.1614991 1.3841249
head(iris.adaboost$votes)
        [,1] [,2] [,3]
[1,] 11.7901    0    0
[2,] 11.7901    0    0
[3,] 11.7901    0    0
[4,] 11.7901    0    0
[5,] 11.7901    0    0
[6,] 11.7901    0    0
data(iris)
iris.boostcv <- boosting.cv(Species ~ ., v=2, data=iris, mfinal=10, control=rpart.control(cp=0.01))
i:  1 Wed Aug  9 15:44:24 2017 
i:  2 Wed Aug  9 15:44:25 2017 
iris.boostcv[-1]
$confusion
               Observed Class
Predicted Class setosa versicolor virginica
     setosa         50          0         0
     versicolor      0         45         1
     virginica       0          5        49

$error
[1] 0.04
seeds <- vector(mode = "list", length = nrow(iris) + 1)
seeds <- lapply(seeds, function(x) 1:20)
grid <- expand.grid(mfinal = (1:3)*3, 
                    maxdepth = c(1, 3),
                    coeflearn = c("Breiman", "Freund", "Zhu"))
cctrl1 <- trainControl(method = "cv", number = 3, returnResamp = "all",
                       classProbs = TRUE, 
                       summaryFunction = multiClassSummary)
                       #, seeds = seeds)
test_class_cv_form <- train(Species ~ ., data = iris, 
                            method = "AdaBoost.M1",
                            tuneGrid = grid, 
                            trControl = cctrl1,
                            metric = "Accuracy", 
                            preProc = c("center", "scale"))
importanceplot(test_class_cv_form$finalModel)

test_class_cv_form$bestTune
iris$pred <- predict(test_class_cv_form, iris)
caret::confusionMatrix(iris$pred, iris$Species)
Confusion Matrix and Statistics

            Reference
Prediction   setosa versicolor virginica
  setosa         50          0         0
  versicolor      0         49         5
  virginica       0          1        45

Overall Statistics
                                         
               Accuracy : 0.96           
                 95% CI : (0.915, 0.9852)
    No Information Rate : 0.3333         
    P-Value [Acc > NIR] : < 2.2e-16      
                                         
                  Kappa : 0.94           
 Mcnemar's Test P-Value : NA             

Statistics by Class:

                     Class: setosa Class: versicolor Class: virginica
Sensitivity                 1.0000            0.9800           0.9000
Specificity                 1.0000            0.9500           0.9900
Pos Pred Value              1.0000            0.9074           0.9783
Neg Pred Value              1.0000            0.9896           0.9519
Prevalence                  0.3333            0.3333           0.3333
Detection Rate              0.3333            0.3267           0.3000
Detection Prevalence        0.3333            0.3600           0.3067
Balanced Accuracy           1.0000            0.9650           0.9450
ensemble method > boosting > random forest ( decision tree forest )
library(randomForest)
credit <- read.csv("/Users/CA/Machine-Learning-with-R-datasets/credit.csv")
credit <- transform(credit, default = ifelse(default == 1, 0, 1))
rf <- randomForest(default ~ ., data = credit, importance = T, proximity = T)
The response has five or fewer unique values.  Are you sure you want to do regression?
rf

Call:
 randomForest(formula = default ~ ., data = credit, importance = T,      proximity = T) 
               Type of random forest: regression
                     Number of trees: 500
No. of variables tried at each split: 6

          Mean of squared residuals: 0.160908
                    % Var explained: 23.38
round(importance(rf), 2)
                     %IncMSE IncNodePurity
checking_balance       40.51         23.75
months_loan_duration   22.28         17.54
credit_history         15.41         12.50
purpose                10.10         18.41
amount                 13.97         23.34
savings_balance        10.20         10.37
employment_length       6.62         11.91
installment_rate        4.47          5.95
personal_status         2.05          6.77
other_debtors          10.16          3.74
residence_history       4.99          5.69
property                6.42          9.13
age                    10.39         16.57
installment_plan        8.89          5.40
housing                 3.21          4.14
existing_credits        3.99          2.93
dependents              0.03          1.85
telephone               2.99          2.40
foreign_worker         -0.76          0.54
job                     5.24          5.62
credit$pred <- predict(rf, credit)
library(InformationValue)
threshold <- optimalCutoff(credit$default, credit$pred)
ModelMetrics::confusionMatrix(credit$default, credit$pred, threshold)
     [,1] [,2]
[1,]  700    0
[2,]    0  300
ensemble method > boosting > xgboost
# ref : http://xgboost.readthedocs.io/en/latest/model.html
# ref : https://medium.com/@peteryun/ml-kaggle%EC%97%90-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EB%8A%94-xgboost-f1650342ba93
library(xgboost)
data("agaricus.test"); data("agaricus.train")
train <- agaricus.train
test  <- agaricus.test
# fit 
model.xgboost <- xgboost(data = train$data, label = train$label,
                         max.depth = 2, eta = 1,
                         nround = 10, nthread = 2, 
                         objective = "binary:logistic")
[1] train-error:0.046522 
[2] train-error:0.022263 
[3] train-error:0.007063 
[4] train-error:0.015200 
[5] train-error:0.007063 
[6] train-error:0.001228 
[7] train-error:0.001228 
[8] train-error:0.001228 
[9] train-error:0.001228 
[10]    train-error:0.000000 
test$pred <- predict(model.xgboost, test$data)
library(InformationValue)
threshold <- optimalCutoff(test$data, test$pred)
misClassError(test$data, test$pred, threshold = threshold)
[1] 0.1762

GBM

# Decision Tree Based
# Boosted : Multiple weak models combined algorithmically
# Gradient Boosted : Iteratively solves residuals
# Stochastic

stacked generalizztion

references

LS0tCnRpdGxlOiAiRW5zZW1ibGUiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCuyekeyEsSDspJEgCgpgYGB7cn0KIyDrjbDsnbTthLAg67aE7ISd7J2AIOyVhOuemOyZgCDqsJnsnYAg7Iic7ISc64yA66GcIOykkeyalO2VmOyngCDslYrsnYTquYwg7ZWY64qUIDIwMTfrhYQg7ZiE7J6sIOyDneqwgS4KIyAzMCUg64+E66mU7J24IOydtO2VtAojIDMwJSBFREEgKyBGRSAKIyAxNSUg642w7J207YSwIOygleygnCAKIyAxMCUg7Y+J6rCAICAgICAgICAKIyAxMCUg66qo642466eBICAgICAgCiMgNSUgIOyVmeyDgeu4lCAgICAgICAKIwojIOydtOykkSDqsJzrs4Qg66qo642466eB7J2EIOygnOyZuO2VnCDrgpjrqLjsp4Drpbwg7JWM7JWE7JW8LCDsmYDqvrjqsIAg7ISc64qUIOqyg+ydtOqzoAojIOqwnOuzhCDrqqjrjbjrp4Eg6riw67KV66eMIOuqh+qwnCDslYTripQg6rKD7J2AIC4uLiDsg53qsIHrs7Tri6Qg642cIOykkeyalO2VoCDsiJgg64+EIOyeiOuLpOuKlCDsg53qsIEuIAojIO2Kue2eiCDrj4TrqZTsnbgg7J207ZW064qUIOyLnOqwhOqzvCDslaDsoJXsnYQg66eO7J20IOyPn+yVhOyVvCDtlZjripQg67aA67aEIOqwmeqzoCwg7Jis67CU66W4IO2PieqwgOyXkOuPhCDrp47snYAg7Iuc6rCE7J2EIOyNqOyVvCDtlZjripQg6rKDIOqwmeuLpC4gKCDsnbQg67aA67aE7J2AIOuztO2GtSDqsITqs7zrkJjquLAg7IqG64ukLiApCmBgYAoKIyMjIyB0ZXJtcyAKYGBge3J9CiMgQmFnZ2luZwojIEJvb3N0aW5nCiMgU3RhY2tlZCBHZW5lcmFsaXphdGlvbgojIAojIFhHQk9PU1QKIyBMQVNBR05FIE5OCiMgQURBQk9PU1QgRVQKIwojIFN0YWNraW5nCiMgR2VuZXJhbGl6YXRpb24gRXJyb3IKIyBtZXRhIGZlYXR1cmVzIC8gb3V0LW9mLWZvbGQgcHJlZGljdGlvbnMKIyBtZXRhIGxlYXJuaW5nCiMKIyBBdmVyYWdlCiMgTGVzcyBWYXJpYW5jZQojIExlc3MgR2VuZXJhbGl6YXRpb24gRXJyb3IKIyBMZXNzIGNoYW5jZSBvZiBvdmVyZml0dGluZwpgYGAKCiMjIyMgbWV0YSBsZWFybmluZwpgYGB7cn0KIyAxLiBtZXRhIGxlYXJuaW5nPwojICAtIGF1dG9tYXRpYyBsZWFybmluZyBhbGdvcml0aG1zIGFyZSBhcHBsaWVkIG9uIG1ldGFkYXRhIGFib3V0IG1hY2hpbmUgbGVhcm5pbmcgZXhwZXJpbWVudHMgCiMKIyAyLiB2YXJpYXRpb25zIG9mIG1ldGEgbGVhcm5pbmcKIyAgKiBzZWxlY3Rpb24gKCBhbGdvcml0aG0gbGVhcm5pbmcgKQojICAqIGh5cGVyLXBhcmFtZXRlciBvcHRpbWl6YXRpb24KIyAgKiBlbnNlbWJsZSAoIGJhZ2dpbmcgLiBib29zdGluZyAuIHN0YWNrZWQgZ2VuZXJhbGl6YXRpb24gKQpgYGAKCiMjIyMgZW5zZW1ibGUKYGBge3J9CmBgYAohW10oL1VzZXJzL0NBL0Rvd25sb2Fkcy9lbnMxLnBuZykKCiMjIyMgZW5zZW1ibGUgbWV0aG9kID4gYmFnZ2luZyAoID0gQm9vdHN0cmFwIEFnZ3JlZ2F0aW5nICkKYGBge3J9CiMgQmFnZ2luZyBnZW5lcmF0ZXMgYSBudW1iZXIgb2YgdHJhaW5pbmcgZGF0YXNldHMgYnkgYm9vdHN0cmFwIHNhbXBsaW5nIHRoZSBvcmlnaW5hbCB0cmFpbmluZyBkYXRhLiAKIyBUaGVzZSBkYXRhc2V0cyBhcmUgdGhlbiB1c2VkIHRvIGdlbmVyYXRlIGEgc2V0IG9mIG1vZGVscyB1c2luZyBhIHNpbmdsZSBsZWFybmluZyBhbGdvcml0aG0uIAojIFRoZSBtb2RlbHMnIHByZWRpY3Rpb25zIGFyZSBjb21iaW5lZCB1c2luZyB2b3RpbmcgKGZvciBjbGFzc2kgY2F0aW9uKSBvciBhdmVyYWdpbmcgKGZvciBudW1lcmljIHByZWRpY3Rpb24pLgojCiMgMS4gQm9vdFN0cmFwIDogdHJhaW5pbmcgZGF0YXNldOycvOuhnOu2gO2EsCDtgazquLDqsIAg6rCZ7J2AIO2RnOuzuOydhCDrsJjrs7XstpTstpwgKGJvb3RzdHJhcCBzYW1wbGluZykKIyAyLiBHZW5lcmF0aW5nIE1vZGVsIDog6rCB6rCB7JeQIOuMgO2VtCDtlZjrgpjsnZggTUwg7JWM6rOg66as7KaY7JeQIOuUsOudvCDrqqjrjbgg7IOd7ISxLCAKIyAzLiBFbnNlbWJsZSA6IOqwgSDrqqjrjbjrk6TsnZgg6rKw6rO866W8IOyihe2Vqe2VmOyXrCDsnZjsgqzqsrDsoJXsnYQg64K066as64qUIOuwqeuylSggdm90aW5nIC8gYXZlcmFnZSApCiMKIyBpdCBjYW4gcGVyZm9ybSBxdWl0ZSB3ZWxsIGFzIGxvbmcgYXMgaXQgaXMgdXNlZCB3aXRoIHJlbGF0aXZlbHkgdW5zdGFibGUgbGVhcm5lcnMsCiMgdGhhdCBpcywgdGhvc2UgZ2VuZXJhdGluZyBtb2RlbHMgdGhhdCB0ZW5kIHRvIGNoYW5nZSBzdWJzdGFudGlhbGx5IHdoZW4gdGhlIGlucHV0IGRhdGEgY2hhbmdlcyBvbmx5IHNsaWdodGx5CiMKIyBiYWdnaW5nIGlzIG9mdGVuIHVzZWQgd2l0aCBkZWNpc2lvbiB0cmVlcywgd2hpY2ggaGF2ZSB0aGUgdGVuZGVuY3kgdG8gdmFyeSBkcmFtYXRpY2FsbHkgZ2l2ZW4gbWlub3IgY2hhbmdlcyBpbiB0aGUgaW5wdXQgZGF0YS4KIwojIHZhcmlhbmNlIOulvCDspITsnbTripQg7Zqo6rO8LgpgYGAKCmBgYHtyfQpsaWJyYXJ5KGlwcmVkKQoKY3JlZGl0IDwtIHJlYWQuY3N2KCIvVXNlcnMvQ0EvTWFjaGluZS1MZWFybmluZy13aXRoLVItZGF0YXNldHMvY3JlZGl0LmNzdiIpCgpzZXQuc2VlZCgxMjM0KQpyZi5iYWcgPC0gYmFnZ2luZyhkZWZhdWx0IH4gLiwgZGF0YSA9IGNyZWRpdCwgbmJhZ2cgPSAyNSkKCmNyZWRpdCRwcmVkIDwtIHByZWRpY3QocmYuYmFnLCBjcmVkaXQpCgpsaWJyYXJ5KEluZm9ybWF0aW9uVmFsdWUpCnRocmVzaG9sZCA8LSBvcHRpbWFsQ3V0b2ZmKGNyZWRpdCRkZWZhdWx0LCBjcmVkaXQkcHJlZCkKY29uZnVzaW9uTWF0cml4KGNyZWRpdCRkZWZhdWx0LCBjcmVkaXQkcHJlZCwgdGhyZXNob2xkID0gdGhyZXNob2xkKQpgYGAKCmBgYHtyfQpsaWJyYXJ5KGNhcmV0KQpjcmVkaXQgPC0gcmVhZC5jc3YoIi9Vc2Vycy9DQS9NYWNoaW5lLUxlYXJuaW5nLXdpdGgtUi1kYXRhc2V0cy9jcmVkaXQuY3N2IikKY3JlZGl0IDwtIHRyYW5zZm9ybShjcmVkaXQsIGRlZmF1bHQgPSBpZmVsc2UoZGVmYXVsdCA9PSAxLCAibm8iLCAieWVzIikpCgpjdHJsIDwtIHRyYWluQ29udHJvbChtZXRob2QgPSAiY3YiLCBudW1iZXIgPSAxMCkKdHJhaW4oZGVmYXVsdCB+IC4sIGRhdGEgPSBjcmVkaXQsIG1ldGhvZCA9ICJ0cmVlYmFnIiwgdHJDb250cm9sID0gY3RybCkKYGBgCgpgYGB7cn0KIyBUaGUgY2FyZXQgcGFja2FnZSBhbHNvIGluY2x1ZGVzIGV4YW1wbGUgb2JqZWN0cyBmb3IgYmFncyBvZiAKIyBuYWl2ZSBCYXllcyBtb2RlbHMgKG5iQmFnKSwgZGVjaXNpb24gdHJlZXMgKGN0cmVlQmFnKSwgYW5kIG5ldXJhbCBuZXR3b3JrcyAobm5ldEJhZykuCnN0cihzdm1CYWcpCmBgYAoKYGBge3J9CmxpYnJhcnkoY2FyZXQpCmxpYnJhcnkoa2VybmxhYikKbGlicmFyeShlMTA3MSkKCmNyZWRpdCA8LSByZWFkLmNzdigiL1VzZXJzL0NBL01hY2hpbmUtTGVhcm5pbmctd2l0aC1SLWRhdGFzZXRzL2NyZWRpdC5jc3YiKQpjcmVkaXQgPC0gdHJhbnNmb3JtKGNyZWRpdCwgZGVmYXVsdCA9IGlmZWxzZShkZWZhdWx0ID09IDEsICJubyIsICJ5ZXMiKSkKCnN2bS5wcmVkaWN0IDwtIGZ1bmN0aW9uIChvYmplY3QsIHgpCnsKIGlmIChpcy5jaGFyYWN0ZXIobGV2KG9iamVjdCkpKSB7CiAgICBvdXQgPC0gcHJlZGljdChvYmplY3QsIGFzLm1hdHJpeCh4KSwgdHlwZSA9ICJwcm9iYWJpbGl0aWVzIikKICAgIGNvbG5hbWVzKG91dCkgPC0gbGV2KG9iamVjdCkKICAgIHJvd25hbWVzKG91dCkgPC0gTlVMTAogIH0KICBlbHNlIG91dCA8LSBwcmVkaWN0KG9iamVjdCwgYXMubWF0cml4KHgpKVssIDFdCiAgb3V0Cn0KCmJhZ0N0cmwgPC0gYmFnQ29udHJvbChmaXQgPSBzdm1CYWckZml0LCBwcmVkaWN0ID0gc3ZtLnByZWRpY3QsIGFnZ3JlZ2F0ZSA9IHN2bUJhZyRhZ2dyZWdhdGUpCnRyQ3RybCAgICA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gImN2IiwgbnVtYmVyID0gMTApCgpzdm1iYWcucmVzdWx0IDwtIHRyYWluKGRlZmF1bHQgfi4sIGRhdGEgPSBjcmVkaXQsIG1ldGhvZCA9ICJiYWciLCB0ckNvbnRyb2wgPSB0ckN0cmwsIGJhZ0NvbnRyb2wgPSBiYWdDdHJsLCB2ZXJib3NlID0gRikKYGBgCgpgYGB7cn0Kc3ZtYmFnLnJlc3VsdApgYGAKCiMjIyMgZW5zZW1ibGUgbWV0aG9kID4gYm9vc3RpbmcKYGBge3J9CiMgaXQgYm9vc3RzIHRoZSBwZXJmb3JtYW5jZSBvZiB3ZWFrIGxlYXJuZXJzIHRvIGF0dGFpbiB0aGUgcGVyZm9ybWFuY2Ugb2Ygc3Ryb25nZXIgbGVhcm5lcnMKIyBib29zdGluZyB1c2VzIGVuc2VtYmxlcyBvZiBtb2RlbHMgdHJhaW5lZCBvbiByZXNhbXBsZWQgZGF0YSBhbmQgYSB2b3RlIHRvIGRldGVybWluZSB0aGUgIG5hbCBwcmVkaWN0aW9uCiMKIyBGaXJzdCwgdGhlIHJlc2FtcGxlZCBkYXRhc2V0cyBpbiBib29zdGluZyBhcmUgY29uc3RydWN0ZWQgc3BlY2ljYWxseSB0byBnZW5lcmF0ZSBjb21wbGVtZW50YXJ5IGxlYXJuZXJzLiAKIyBTZWNvbmQsIHJhdGhlciB0aGFuIGdpdmluZyBlYWNoIGxlYXJuZXIgYW4gZXF1YWwgdm90ZSwgYm9vc3RpbmcgZ2l2ZXMgZWFjaCBsZWFybmVyJ3Mgdm90ZSBhIHdlaWdodCBiYXNlZCBvbiBpdHMgcGFzdCBwZXJmb3JtYW5jZS4KIyAKIyB2YXJpYW5jZSDsmYAg7ZWo6ruYLCBiaWFzIOuPhCDspITsl6zqsIDripQg7Zqo6rO8ICh3ZWFrIGxlYXJuZXIgLyBzdHJvbmcgbGVhcm5lciDqsIQgd2VpZ2h0IOyhsOyglSkKYGBgCgoKYGBge3J9CmxpYnJhcnkocmFuZG9tRm9yZXN0KQpsaWJyYXJ5KGRhdGEudGFibGUpCmxpYnJhcnkoZ2JtKQpsaWJyYXJ5KGdncGxvdDIpCmxpYnJhcnkocGx5cikKbGlicmFyeShkcGx5cikKbGlicmFyeShycGFydCkKCnggPC0gc2VxKC0yLDIsYnk9MC4wMSkKbGVueDwtIGxlbmd0aCh4KQp5IDwtIDIgKyAzKnheMiArIHJub3JtKGxlbngsIDAsIDAuNSkKeV9yIDwtIDIgKyAzKnheMgp4LnkgPC0gZGF0YS5mcmFtZSh4PXgseT15LCB5X3I9eV9yKQoKeC55LnNhbXAgPC0geC55ICU+JSBzYW1wbGVfZnJhYygwLjUpCngueS5zYW1wLnRlc3QgPC0geC55ICU+JSBzYW1wbGVfZnJhYygwLjEpCgptZGxfY2FydCA8LSBycGFydCh5IH4geCwgZGF0YT14Lnkuc2FtcCkKeC55LnNhbXAudGVzdCRjYXJ0X2ZpdCA8LSBwcmVkaWN0KG1kbF9jYXJ0LCBuZXdkYXRhPXgueS5zYW1wLnRlc3QpCgptZGxfcmYgPC0gcmFuZG9tRm9yZXN0KHkgfiB4LGRhdGE9eC55LnNhbXApCngueS5zYW1wLnRlc3QkcmZfZml0IDwtIHByZWRpY3QobWRsX3JmLG5ld2RhdGE9eC55LnNhbXAudGVzdCkKCmdncGxvdCh4Lnkuc2FtcCwgYWVzKHgseV9yKSkgKyBnZW9tX2xpbmUoc2l6ZT0xLjUsIGNvbG91cj0nYmxhY2snKSArIGdlb21fcG9pbnQoYWVzKHk9eSksIHNpemU9MSkgKyBnZW9tX2xpbmUoZGF0YSA9ICB4Lnkuc2FtcC50ZXN0LCBhZXMoeD14LCB5PWNhcnRfZml0KSwgY29sb3VyID0gImJsdWUiKSArIGdlb21fbGluZShkYXRhID0gIHgueS5zYW1wLnRlc3QsIGFlcyh4PXgsIHk9cmZfZml0KSwgY29sb3VyID0gInJlZCIpCmBgYAoKYGBge3J9CnNocmluayA8LSAwLjEKCiNyZWdyZXNzaW9uIGJhc2VkIGJvb3N0aW5nCnlfbiA8LSB4Lnkuc2FtcCR5CnggICA8LSB4Lnkuc2FtcCR4CnZfeV9sIDwtIGxpc3QoKQoKZm9yKGkgaW4gMToxMDApewogIGxtX2ZpdCA8LSBsbSh5X24gfiB4KkkoMCA8IHgpKQogIHZfeSA8LSBzaHJpbmsgKiBwcmVkaWN0KGxtX2ZpdCkKICB2X3lfbFtbaV1dIDwtIHNocmluayAqIHByZWRpY3QobG1fZml0LCBuZXdkYXRhPXgueS5zYW1wLnRlc3QpCiAgcmVzaWRfbiA8LSAgeV9uIC0gdl95CiAgeV9uIDwtIHJlc2lkX24KfQoKeC55LnNhbXAudGVzdCRsbV9maXQgICA8LSBhcHBseShhcy5kYXRhLnRhYmxlKHZfeV9sKSwxLHN1bSkKCngueS5zYW1wLnRlc3QkbG1fZml0XzMgPC0gYXBwbHkoYXMuZGF0YS50YWJsZSh2X3lfbClbLDE6MTAsd2l0aD1GXSwxLHN1bSkKCngueS5zYW1wLnRlc3QkbG1fZml0XzIgPC0gYXBwbHkoYXMuZGF0YS50YWJsZSh2X3lfbClbLDE6NSx3aXRoPUZdLDEsc3VtKQoKeC55LnNhbXAudGVzdCRsbV9maXRfMSA8LSBhcHBseShhcy5kYXRhLnRhYmxlKHZfeV9sKVssMToyLHdpdGg9Rl0sMSxzdW0pCgpnZ3Bsb3QoeC55LnNhbXAsIGFlcyh4PXgseT15X3IpKSArIGdlb21fbGluZShzaXplPTEuNSwgY29sb3VyPSdyZWQnKSArIGdlb21fcG9pbnQoYWVzKHk9eSksIHNpemU9MSkgKyBnZW9tX2xpbmUoZGF0YT14Lnkuc2FtcC50ZXN0LCBhZXMoeD14LHk9bG1fZml0KSwgY29sb3VyPSdwdXJwbGUnLCBsaW5ldHlwZT0yLCBzaXplPTEpICsgZ2VvbV9saW5lKGRhdGE9eC55LnNhbXAudGVzdCwgYWVzKHg9eCx5PWxtX2ZpdF8yKSxjb2xvdXI9J3B1cnBsZScsbGluZXR5cGU9NCkgKyBnZW9tX2xpbmUoZGF0YT14Lnkuc2FtcC50ZXN0LCBhZXMoeD14LHk9bG1fZml0XzMpLGNvbG91cj0ncHVycGxlJyxsaW5ldHlwZT00KSArIGdlb21fbGluZShkYXRhPXgueS5zYW1wLnRlc3QsYWVzKHg9eCx5PWxtX2ZpdF8xKSxjb2xvdXI9J3B1cnBsZScsbGluZXR5cGU9NCkKYGBgCgpgYGB7cn0KI2NhcnQgYmFzZWQgYm9vc3RpbmcKeV9uIDwtIHgueS5zYW1wJHkKeCA8LSB4Lnkuc2FtcCR4CnZfeV9sIDwtIGxpc3QoKQpmb3IoaSBpbiAxOjEwMCl7CiAgcnBhcnRfZml0IDwtIHJwYXJ0KHlfbiB+IHgpCiAgdl95IDwtIHNocmluayAqIHByZWRpY3QocnBhcnRfZml0KQogIHZfeV9sW1tpXV0gPC0gc2hyaW5rICogcHJlZGljdChycGFydF9maXQsIG5ld2RhdGE9eC55LnNhbXAudGVzdCkKICByZXNpZF9uIDwtICB5X24gLSB2X3kKICB5X24gPC0gcmVzaWRfbgp9CgoKCngueS5zYW1wLnRlc3QkcnBhcnRfZml0IDwtIGFwcGx5KGFzLmRhdGEudGFibGUodl95X2wpLDEsc3VtKQoKeC55LnNhbXAudGVzdCRycGFydF9maXRfMyA8LSBhcHBseShhcy5kYXRhLnRhYmxlKHZfeV9sKVssMToxMCx3aXRoPUZdLDEsc3VtKQoKeC55LnNhbXAudGVzdCRycGFydF9maXRfMiA8LSBhcHBseShhcy5kYXRhLnRhYmxlKHZfeV9sKVssMTo1LHdpdGg9Rl0sMSxzdW0pCgp4Lnkuc2FtcC50ZXN0JHJwYXJ0X2ZpdF8xIDwtIGFwcGx5KGFzLmRhdGEudGFibGUodl95X2wpWywxOjIsd2l0aD1GXSwxLHN1bSkKCgpnZ3Bsb3QoeC55LnNhbXAsIGFlcyh4PXgseT15X3IpKSArIGdlb21fbGluZShzaXplPTEuNSwgY29sb3VyPSdyZWQnKSArIGdlb21fcG9pbnQoYWVzKHk9eSksIHNpemU9MSkgKyBnZW9tX2xpbmUoZGF0YT14Lnkuc2FtcC50ZXN0LCBhZXMoeD14LHk9cnBhcnRfZml0KSwgY29sb3VyPSdwdXJwbGUnLCBsaW5ldHlwZT0yLCBzaXplPTEpICsgZ2VvbV9saW5lKGRhdGE9eC55LnNhbXAudGVzdCwgYWVzKHg9eCx5PXJwYXJ0X2ZpdF8yKSxjb2xvdXI9J3B1cnBsZScsbGluZXR5cGU9NCkgKyBnZW9tX2xpbmUoZGF0YT14Lnkuc2FtcC50ZXN0LCBhZXMoeD14LHk9cnBhcnRfZml0XzMpLGNvbG91cj0ncHVycGxlJyxsaW5ldHlwZT00KSArIGdlb21fbGluZShkYXRhPXgueS5zYW1wLnRlc3QsYWVzKHg9eCx5PXJwYXJ0X2ZpdF8xKSxjb2xvdXI9J3B1cnBsZScsbGluZXR5cGU9NCkgCgpgYGAKCiMjIyMjIGVuc2VtYmxlIG1ldGhvZCA+IGJvb3N0aW5nID4gYWRhcHRpdmUgYm9vc3RpbmcgPiBBZGFCb29zdApgYGB7cn0KIyBBZGFCb29zdC5NMSBhbGdvcml0aG0gOiBhZGFiYWcgcGFjYWtnZQpsaWJyYXJ5KGFkYWJhZykKZGF0YShpcmlzKQoKaXJpcy5hZGFib29zdCA8LSBib29zdGluZyhTcGVjaWVzfi4sIGRhdGEgPSBpcmlzLCBib29zID0gVFJVRSwgbWZpbmFsID0gMTAsIGNvZWZsZWFybiA9ICJCcmVpbWFuIikKaW1wb3J0YW5jZXBsb3QoaXJpcy5hZGFib29zdCkKaXJpcy5hZGFib29zdCR3ZWlnaHRzCmhlYWQoaXJpcy5hZGFib29zdCR2b3RlcykKYGBgCgoKYGBge3J9CmRhdGEoaXJpcykKaXJpcy5ib29zdGN2IDwtIGJvb3N0aW5nLmN2KFNwZWNpZXMgfiAuLCB2PTIsIGRhdGE9aXJpcywgbWZpbmFsPTEwLCBjb250cm9sPXJwYXJ0LmNvbnRyb2woY3A9MC4wMSkpCmlyaXMuYm9vc3RjdlstMV0KYGBgCgpgYGB7cn0Kc2VlZHMgPC0gdmVjdG9yKG1vZGUgPSAibGlzdCIsIGxlbmd0aCA9IG5yb3coaXJpcykgKyAxKQpzZWVkcyA8LSBsYXBwbHkoc2VlZHMsIGZ1bmN0aW9uKHgpIDE6MjApCgpncmlkIDwtIGV4cGFuZC5ncmlkKG1maW5hbCA9ICgxOjMpKjMsIAogICAgICAgICAgICAgICAgICAgIG1heGRlcHRoID0gYygxLCAzKSwKICAgICAgICAgICAgICAgICAgICBjb2VmbGVhcm4gPSBjKCJCcmVpbWFuIiwgIkZyZXVuZCIsICJaaHUiKSkKCmNjdHJsMSA8LSB0cmFpbkNvbnRyb2wobWV0aG9kID0gImN2IiwgbnVtYmVyID0gMywgcmV0dXJuUmVzYW1wID0gImFsbCIsCiAgICAgICAgICAgICAgICAgICAgICAgY2xhc3NQcm9icyA9IFRSVUUsIAogICAgICAgICAgICAgICAgICAgICAgIHN1bW1hcnlGdW5jdGlvbiA9IG11bHRpQ2xhc3NTdW1tYXJ5KQogICAgICAgICAgICAgICAgICAgICAgICMsIHNlZWRzID0gc2VlZHMpCgp0ZXN0X2NsYXNzX2N2X2Zvcm0gPC0gdHJhaW4oU3BlY2llcyB+IC4sIGRhdGEgPSBpcmlzLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1ldGhvZCA9ICJBZGFCb29zdC5NMSIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICB0dW5lR3JpZCA9IGdyaWQsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgdHJDb250cm9sID0gY2N0cmwxLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0cmljID0gIkFjY3VyYWN5IiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBwcmVQcm9jID0gYygiY2VudGVyIiwgInNjYWxlIikpCmltcG9ydGFuY2VwbG90KHRlc3RfY2xhc3NfY3ZfZm9ybSRmaW5hbE1vZGVsKQp0ZXN0X2NsYXNzX2N2X2Zvcm0kYmVzdFR1bmUKCmlyaXMkcHJlZCA8LSBwcmVkaWN0KHRlc3RfY2xhc3NfY3ZfZm9ybSwgaXJpcykKY2FyZXQ6OmNvbmZ1c2lvbk1hdHJpeChpcmlzJHByZWQsIGlyaXMkU3BlY2llcykKYGBgCgojIyMjIyBlbnNlbWJsZSBtZXRob2QgPiBib29zdGluZyA+IHJhbmRvbSBmb3Jlc3QgKCBkZWNpc2lvbiB0cmVlIGZvcmVzdCApCmBgYHtyfQojIGEuay5hIOusu+yngOuniCDrqqjrjbggCiMKIyBhZGEgYm9vc3Qg64qUIOyXrOufrCDrqqjrjbjsl5Ag7KCB7Jqp7J20IOqwgOuKpe2VnCDrqqjrjbjsnbgg67CY66m0LCByYW5kb20gZm9yZXN0IOuKlCBkZWNpc2lvbiB0cmVlIOuTpOydmCBlbnNlbWJsZSDrp4wg6rCA64qlIAojCiMgQWZ0ZXIgdGhlIGVuc2VtYmxlIG9mIHRyZWVzICh0aGUgZm9yZXN0KSBpcyBnZW5lcmF0ZWQsIAojIHRoZSBtb2RlbCB1c2VzIGEgdm90ZSB0byBjb21iaW5lIHRoZSB0cmVlcycgcHJlZGljdGlvbnMuCmBgYAohW10oL1VzZXJzL0NBL0Rvd25sb2Fkcy9yZi5wbmcpCgpgYGB7cn0KbGlicmFyeShyYW5kb21Gb3Jlc3QpCmNyZWRpdCA8LSByZWFkLmNzdigiL1VzZXJzL0NBL01hY2hpbmUtTGVhcm5pbmctd2l0aC1SLWRhdGFzZXRzL2NyZWRpdC5jc3YiKQpjcmVkaXQgPC0gdHJhbnNmb3JtKGNyZWRpdCwgZGVmYXVsdCA9IGlmZWxzZShkZWZhdWx0ID09IDEsIDAsIDEpKQoKcmYgPC0gcmFuZG9tRm9yZXN0KGRlZmF1bHQgfiAuLCBkYXRhID0gY3JlZGl0LCBpbXBvcnRhbmNlID0gVCwgcHJveGltaXR5ID0gVCkKcmYKCnJvdW5kKGltcG9ydGFuY2UocmYpLCAyKQoKY3JlZGl0JHByZWQgPC0gcHJlZGljdChyZiwgY3JlZGl0KQoKbGlicmFyeShJbmZvcm1hdGlvblZhbHVlKQp0aHJlc2hvbGQgPC0gb3B0aW1hbEN1dG9mZihjcmVkaXQkZGVmYXVsdCwgY3JlZGl0JHByZWQpCk1vZGVsTWV0cmljczo6Y29uZnVzaW9uTWF0cml4KGNyZWRpdCRkZWZhdWx0LCBjcmVkaXQkcHJlZCwgdGhyZXNob2xkKQpgYGAKCiMjIyMjIGVuc2VtYmxlIG1ldGhvZCA+IGJvb3N0aW5nID4geGdib29zdAoKYGBge3J9CiMgcmVmIDogaHR0cDovL3hnYm9vc3QucmVhZHRoZWRvY3MuaW8vZW4vbGF0ZXN0L21vZGVsLmh0bWwKIyByZWYgOiBodHRwczovL21lZGl1bS5jb20vQHBldGVyeXVuL21sLWthZ2dsZSVFQyU5NyU5MC0lRUMlQTAlODElRUMlOUElQTklRUQlOTUlQjQlRUIlQjMlQjQlRUIlOEElOTQteGdib29zdC1mMTY1MDM0MmJhOTMKCmxpYnJhcnkoeGdib29zdCkKCmRhdGEoImFnYXJpY3VzLnRlc3QiKTsgZGF0YSgiYWdhcmljdXMudHJhaW4iKQoKdHJhaW4gPC0gYWdhcmljdXMudHJhaW4KdGVzdCAgPC0gYWdhcmljdXMudGVzdAoKIyBmaXQgCm1vZGVsLnhnYm9vc3QgPC0geGdib29zdChkYXRhID0gdHJhaW4kZGF0YSwgbGFiZWwgPSB0cmFpbiRsYWJlbCwKICAgICAgICAgICAgICAgICAgICAgICAgIG1heC5kZXB0aCA9IDIsIGV0YSA9IDEsCiAgICAgICAgICAgICAgICAgICAgICAgICBucm91bmQgPSAxMCwgbnRocmVhZCA9IDIsIAogICAgICAgICAgICAgICAgICAgICAgICAgb2JqZWN0aXZlID0gImJpbmFyeTpsb2dpc3RpYyIpCgp0ZXN0JHByZWQgPC0gcHJlZGljdChtb2RlbC54Z2Jvb3N0LCB0ZXN0JGRhdGEpCgpsaWJyYXJ5KEluZm9ybWF0aW9uVmFsdWUpCnRocmVzaG9sZCA8LSBvcHRpbWFsQ3V0b2ZmKHRlc3QkZGF0YSwgdGVzdCRwcmVkKQptaXNDbGFzc0Vycm9yKHRlc3QkZGF0YSwgdGVzdCRwcmVkLCB0aHJlc2hvbGQgPSB0aHJlc2hvbGQpCmBgYAoKIyMjIyBHQk0KYGBge3J9CiMgRGVjaXNpb24gVHJlZSBCYXNlZAojIEJvb3N0ZWQgOiBNdWx0aXBsZSB3ZWFrIG1vZGVscyBjb21iaW5lZCBhbGdvcml0aG1pY2FsbHkKIyBHcmFkaWVudCBCb29zdGVkIDogSXRlcmF0aXZlbHkgc29sdmVzIHJlc2lkdWFscwojIFN0b2NoYXN0aWMKYGBgCgojIyMjIHN0YWNrZWQgZ2VuZXJhbGl6enRpb24KYGBge3J9CmBgYAoKCgojIyMjIHJlZmVyZW5jZXMKYGBge3J9CiMgW3RleHRib29rXSBtYWNoaW5lIGxlYXJuaW5nIHdpdGggUiwgQ0ggMTEKIyBbdWJjIGxlY3R1cmVdIGh0dHA6Ly93d3cuY3MudWJjLmNhL2xhYnMvYmV0YS9Db3Vyc2VzL0NQU0M1MzJILTEzL1NsaWRlcy9jb250ZW50LXNlc3Npb24tNC1zbGlkZXMucGRmCiMgW2Jvb3N0aW5nIHZzIGJhZ2dpbmddIGh0dHA6Ly9mcmVlc2VhcmNoLnBlLmtyL2FyY2hpdmVzLzQzNDkKIyBbQSBib29zdGluZyBhIG5ldyBpZGVhIG9mIGJ1aWxkaW5nIG1vZGVsc10gaHR0cHM6Ly9wZGZzLnNlbWFudGljc2Nob2xhci5vcmcvOGQ4MS9iN2U5MzAyODRiZWNhZTljMzM4MmJhMzNjNGQ0OGM3ZTU5OGEucGRmCiMgW+uLqOyInOywuOqzoDogU0FTXSBodHRwczovL3d3dy5zYXMuY29tL2NvbnRlbnQvZGFtL1NBUy9rb19rci9kb2MvcHJvZHVjdGJyaWVmL01hY2hpbmVfbGVhcm5pbmdfd2l0aF9TQVNfRW1pbmVyXzE2UC5wZGYgCiMgW2dibSBfIGthZ2dsZSB0aXRhbmljXSBodHRwczovL3d3dy5yLWJsb2dnZXJzLmNvbS9wcmVkaWN0aW5nLXRpdGFuaWMtZGVhdGhzLW9uLWthZ2dsZS1paS1nYm0vCmBgYA==