Bài viết này sử dụng hàm naiveBayes() trong package e1071 để phân tích classification theo trường phái Bayes và so sánh với hàm stan_glm() của package rstan mà Bs Nhi đã trình bày trong bài trước.

Mục đích của bài viết này là so sánh kết quả của hai hàm cùng theo trường phái Bayes trong phân tích hồi qui logistic và phân loại.

Dữ liệu dùng để phân tích trong bài này chính là bộ dữ liệu trong bài trước của Bs Nhi. Bài viết gồm hai phần: stan_glm() và sau đó là naiveBayes()

1 Bayes Logistic Regression với stan_glm()

Chuẩn bị dữ liệu:

## # A tibble: 6 x 10
##   clumpthickness SizeUniformity ShapeUniformity Margin_adhesion
##            <int>          <int>           <int>           <int>
## 1              5              1               1               1
## 2              5              4               4               5
## 3              3              1               1               1
## 4              6              8               8               1
## 5              4              1               1               3
## 6              8             10              10               8
## # ... with 6 more variables: EpiCellSize <int>, Barenuclei <int>,
## #   BlandChromatin <int>, NormalNucleoli <int>, Mitoses <int>,
## #   Class <fctr>

Xây dựng mô hình hồi qui với hàm stan_glm:

library(caret)
set.seed(123)
id=createDataPartition(y=df2$Class, p=499/683,list=FALSE)
trainset=df2[id,]
testset=df2[-id,]

library(rstanarm)
library(rstan)
t_prior=student_t(df = 5, location = 0, scale = 2.5)
fml="Class~clumpthickness+
ShapeUniformity+
Margin_adhesion+
Barenuclei+
BlandChromatin"

fittprior=stan_glm(
  fml,
  data = trainset, 
  family = binomial(link = "logit"), 
  prior = t_prior,
  prior_intercept = t_prior,
  algorithm="sampling",
  chains = 2, iter = 1500,warmup = 500,
  cores=parallel::detectCores(),
  seed=123
)

Trình bày kết quả tóm tắt:

summary(fittprior,digits=5,probs=c(0.025,0.975))
## 
## Model Info:
## 
##  function:  stan_glm
##  family:    binomial [logit]
##  formula:   "Class~clumpthickness+\nShapeUniformity+\nMargin_adhesion+\nBarenuclei+\nBlandChromatin"
##  algorithm: sampling
##  priors:    see help('prior_summary')
##  sample:    2000 (posterior sample size)
##  num obs:   500
## 
## Estimates:
##                   mean      sd        2.5%      97.5%  
## (Intercept)      -9.38167   1.07077 -11.68469  -7.44547
## clumpthickness    0.60094   0.13871   0.34474   0.88336
## ShapeUniformity   0.51179   0.15944   0.22457   0.85374
## Margin_adhesion   0.32699   0.12201   0.09571   0.57412
## Barenuclei        0.36338   0.09578   0.18348   0.55757
## BlandChromatin    0.46135   0.17167   0.13267   0.81154
## mean_PPD          0.35059   0.01022   0.33000   0.37000
## log-posterior   -58.99672   1.70967 -63.18537 -56.54665
## 
## Diagnostics:
##                 mcse    Rhat    n_eff
## (Intercept)     0.02633 0.99935 1654 
## clumpthickness  0.00352 0.99904 1551 
## ShapeUniformity 0.00382 1.00010 1741 
## Margin_adhesion 0.00300 1.00143 1655 
## Barenuclei      0.00254 0.99905 1423 
## BlandChromatin  0.00395 0.99926 1888 
## mean_PPD        0.00023 1.00011 2000 
## log-posterior   0.06419 1.00059  709 
## 
## For each parameter, mcse is Monte Carlo standard error, n_eff is a crude measure of effective sample size, and Rhat is the potential scale reduction factor on split chains (at convergence Rhat=1).
testpred=predict(fittprior,newdata=testset,type="link")%>%exp()
testset$Truth=ifelse(testset$Class==1,"Malignant","Benign")
testset$Pred=testpred
testset$Pred=ifelse(testset$Pred>=0.5,"Malignant","Benign")

caret::confusionMatrix(testset$Pred,reference=testset$Truth,mode="everything",positive="Malignant")
## Confusion Matrix and Statistics
## 
##            Reference
## Prediction  Benign Malignant
##   Benign       119         1
##   Malignant      0        63
##                                           
##                Accuracy : 0.9945          
##                  95% CI : (0.9699, 0.9999)
##     No Information Rate : 0.6503          
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.9879          
##  Mcnemar's Test P-Value : 1               
##                                           
##             Sensitivity : 0.9844          
##             Specificity : 1.0000          
##          Pos Pred Value : 1.0000          
##          Neg Pred Value : 0.9917          
##               Precision : 1.0000          
##                  Recall : 0.9844          
##                      F1 : 0.9921          
##              Prevalence : 0.3497          
##          Detection Rate : 0.3443          
##    Detection Prevalence : 0.3443          
##       Balanced Accuracy : 0.9922          
##                                           
##        'Positive' Class : Malignant       
## 

2 Bayes classification với naiveBayes()

Đến đây chúng ta load package e1071 để chạy hàm naiveBayes() cho cùng một mô hình như trên. Pred2 là predictions cho test dataset. Hãy nhìn vào kết quả phân tích confusion matrix của Pred2 để so sánh với kết quả phân tích với hàm stan_glm().

library(e1071)

fml2 <- Class ~ clumpthickness + SizeUniformity + ShapeUniformity + Margin_adhesion +
  EpiCellSize + Barenuclei + BlandChromatin + NormalNucleoli + Mitoses

fit <- naiveBayes(fml2, data = trainset, laplace = 0, na.action = na.pass)

# trainset
pred1 <- predict(fit, newdata = trainset, type = "class" )
table(pred1,trainset$Class)
##      
## pred1   0   1
##     0 309   3
##     1  16 172
confusionMatrix(pred1, trainset$Class,  positive = "1")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 309   3
##          1  16 172
##                                          
##                Accuracy : 0.962          
##                  95% CI : (0.9413, 0.977)
##     No Information Rate : 0.65           
##     P-Value [Acc > NIR] : < 2.2e-16      
##                                          
##                   Kappa : 0.9179         
##  Mcnemar's Test P-Value : 0.005905       
##                                          
##             Sensitivity : 0.9829         
##             Specificity : 0.9508         
##          Pos Pred Value : 0.9149         
##          Neg Pred Value : 0.9904         
##              Prevalence : 0.3500         
##          Detection Rate : 0.3440         
##    Detection Prevalence : 0.3760         
##       Balanced Accuracy : 0.9668         
##                                          
##        'Positive' Class : 1              
## 
#testset
pred2 <- predict(fit, newdata = testset, type = "class" )
table(pred2,testset$Class)
##      
## pred2   0   1
##     0 114   2
##     1   5  62
confusionMatrix(pred2, testset$Class,  positive = "1")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction   0   1
##          0 114   2
##          1   5  62
##                                           
##                Accuracy : 0.9617          
##                  95% CI : (0.9228, 0.9845)
##     No Information Rate : 0.6503          
##     P-Value [Acc > NIR] : <2e-16          
##                                           
##                   Kappa : 0.9168          
##  Mcnemar's Test P-Value : 0.4497          
##                                           
##             Sensitivity : 0.9688          
##             Specificity : 0.9580          
##          Pos Pred Value : 0.9254          
##          Neg Pred Value : 0.9828          
##              Prevalence : 0.3497          
##          Detection Rate : 0.3388          
##    Detection Prevalence : 0.3661          
##       Balanced Accuracy : 0.9634          
##                                           
##        'Positive' Class : 1               
## 

Accuracy là 0.9617 so với 0.9945 của hàm stan_glm và Sensitivity cũng như Specificity đều thấp hơn một ít so với kết quả phân tích bằng hàm stan_glm().

3 ROC cho hai mô hình phân tích

ROC cho mô hình với naiveBayes

library(ROCR)
library(ggplot2)
# ROC for naiveBayes
xTest <- testset[,-10]
probs <- predict(fit, xTest, type="raw")
pred3 <- prediction(probs[, "1"], testset$Class)
perf_nb <- performance(pred3, measure='tpr', x.measure='fpr')
plot(perf_nb)

performance(pred3, 'auc')
## An object of class "performance"
## Slot "x.name":
## [1] "None"
## 
## Slot "y.name":
## [1] "Area under the ROC curve"
## 
## Slot "alpha.name":
## [1] "none"
## 
## Slot "x.values":
## list()
## 
## Slot "y.values":
## [[1]]
## [1] 0.9891019
## 
## 
## Slot "alpha.values":
## list()

ROC cho mô hình với Bayes Logistic Regression:

# ROC for logistic Bayes
pred <- prediction(testpred, testset$Class)
perf_lr <- performance(pred, measure='tpr', x.measure='fpr')
plot(perf_lr)

So sánh hai đường cong ROC cho hai mô hình ở trên: Đường cong ROC của mô hình bayes với hàm stan_glm() cho thấy có mức độ chính xác cao hơn.

roc_nb <- data.frame(fpr=unlist(perf_nb@x.values), tpr=unlist(perf_nb@y.values))
roc_nb$method <- "naive Bayes"
roc_lr <- data.frame(fpr=unlist(perf_lr@x.values), tpr=unlist(perf_lr@y.values))
roc_lr$method <- "Bayes logistic regression"
rbind(roc_nb, roc_lr) %>%
  ggplot(data=., aes(x=fpr, y=tpr, linetype=method, color=method)) + 
  geom_line() +
  geom_abline(a=1, b=0, linetype=2) +
  scale_x_continuous(lim=c(0,1)) +
  scale_y_continuous(lim=c(0,1)) +
  theme(legend.position=c(0.8,0.2), legend.title=element_blank())

Việc phân tích và so sánh hai mô hình classification theo trường phái Bayes nhưng từ hai package khác nhau để mở rộng sự hiều biết của chúng ta về các hàm khác nhau về phương pháp phân tích. Chúng ta thấy phương pháp Naive Bayes classification thường cho kết quả phân loại với mức độ chính xác thấp hơn một ít so với phương pháp Logistic Regression, tuy rằng cùng trường phái Bayes.

Rất mong nhận được ý kiến của các anh chị, các bạn về sự so sánh trên để hiểu rõ hơn về chủ đề này và cám ơn mọi người đã dành thời gian đọc bài viết.

LS0tDQp0aXRsZTogJ0JheWVzIENsYXNzaWZpY2F0aW9uOiBTbyBTw6FuaCBuYWl2ZUJheWVzIHZzLiBzdGFuX2dsbScNCmF1dGhvcjogIk5ndXllbiBOZ29jIFRoaWV1Ig0KZGF0ZTogIlNlcHRlbWJlciAxMywgMjAxNyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJkZWZhdWx0Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0KYGBgDQoNCkLDoGkgdmnhur90IG7DoHkgc+G7rSBk4bulbmcgaMOgbSBuYWl2ZUJheWVzKCkgdHJvbmcgcGFja2FnZSBlMTA3MSDEkeG7gyBwaMOibiB0w61jaCBjbGFzc2lmaWNhdGlvbiB0aGVvIHRyxrDhu51uZyBwaMOhaSBCYXllcyB2w6Agc28gc8OhbmggduG7m2kgaMOgbSBzdGFuX2dsbSgpIGPhu6dhIHBhY2thZ2UgcnN0YW4gbcOgIEJzIE5oaSDEkcOjIHRyw6xuaCBiw6B5IHRyb25nIGLDoGkgdHLGsOG7m2MuIA0KDQpN4bulYyDEkcOtY2ggY+G7p2EgYsOgaSB2aeG6v3QgbsOgeSBsw6Agc28gc8Ohbmgga+G6v3QgcXXhuqMgY+G7p2EgaGFpIGjDoG0gY8O5bmcgdGhlbyB0csaw4budbmcgcGjDoWkgQmF5ZXMgdHJvbmcgcGjDom4gdMOtY2ggaOG7k2kgcXVpIGxvZ2lzdGljIHbDoCBwaMOibiBsb+G6oWkuDQoNCkThu68gbGnhu4d1IGTDuW5nIMSR4buDIHBow6JuIHTDrWNoIHRyb25nIGLDoGkgbsOgeSBjaMOtbmggbMOgIGLhu5kgZOG7ryBsaeG7h3UgdHJvbmcgYsOgaSB0csaw4bubYyBj4bunYSBCcyBOaGkuIELDoGkgdmnhur90IGfhu5NtIGhhaSBwaOG6p246IHN0YW5fZ2xtKCkgdsOgIHNhdSDEkcOzIGzDoCBuYWl2ZUJheWVzKCkNCg0KIyBCYXllcyBMb2dpc3RpYyBSZWdyZXNzaW9uIHbhu5tpIHN0YW5fZ2xtKCkNCg0KQ2h14bqpbiBi4buLIGThu68gbGnhu4d1Og0KDQpgYGB7ciBwcmVzc3VyZSwgZWNobz1GQUxTRSwgbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCg0KZGY9cmVhZC5jc3YoImh0dHA6Ly92aW5jZW50YXJlbGJ1bmRvY2suZ2l0aHViLmlvL1JkYXRhc2V0cy9jc3YvTUFTUy9iaW9wc3kuY3N2IiklPiVhc190aWJibGUoKSU+JS5bLGMoMzoxMildJT4lbmEub21pdCgpDQpuYW1lcyhkZik9YygiY2x1bXB0aGlja25lc3MiLA0KICAgICAgICAgICAgIlNpemVVbmlmb3JtaXR5IiwNCiAgICAgICAgICAgICJTaGFwZVVuaWZvcm1pdHkiLA0KICAgICAgICAgICAgIk1hcmdpbl9hZGhlc2lvbiIsDQogICAgICAgICAgICAiRXBpQ2VsbFNpemUiLA0KICAgICAgICAgICAgIkJhcmVudWNsZWkiLA0KICAgICAgICAgICAgIkJsYW5kQ2hyb21hdGluIiwNCiAgICAgICAgICAgICJOb3JtYWxOdWNsZW9saSIsDQogICAgICAgICAgICAiTWl0b3NlcyIsDQogICAgICAgICAgICAiQ2xhc3MiICkNCg0KZGYyPWRmJT4lbXV0YXRlKC4sQ2xhc3M9YXMuaW50ZWdlciguJENsYXNzKS0xTCkNCmRmMiRDbGFzc1tkZjIkQ2xhc3MgPT0gImJlbmlnbiJdIDwtIDANCmRmMiRDbGFzc1tkZjIkQ2xhc3MgPT0gIm1hbGlnbmFudCJdIDwtIDENCmRmMiRDbGFzcyA8LSBhcy5mYWN0b3IoZGYyJENsYXNzKQ0KaGVhZChkZjIpDQoNCmBgYA0KDQpYw6J5IGThu7FuZyBtw7QgaMOsbmggaOG7k2kgcXVpIHbhu5tpIGjDoG0gc3Rhbl9nbG06DQoNCmBgYHtyIG1lc3NhZ2UgPSBGQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoY2FyZXQpDQpzZXQuc2VlZCgxMjMpDQppZD1jcmVhdGVEYXRhUGFydGl0aW9uKHk9ZGYyJENsYXNzLCBwPTQ5OS82ODMsbGlzdD1GQUxTRSkNCnRyYWluc2V0PWRmMltpZCxdDQp0ZXN0c2V0PWRmMlstaWQsXQ0KDQpsaWJyYXJ5KHJzdGFuYXJtKQ0KbGlicmFyeShyc3RhbikNCnRfcHJpb3I9c3R1ZGVudF90KGRmID0gNSwgbG9jYXRpb24gPSAwLCBzY2FsZSA9IDIuNSkNCmZtbD0iQ2xhc3N+Y2x1bXB0aGlja25lc3MrDQpTaGFwZVVuaWZvcm1pdHkrDQpNYXJnaW5fYWRoZXNpb24rDQpCYXJlbnVjbGVpKw0KQmxhbmRDaHJvbWF0aW4iDQoNCmZpdHRwcmlvcj1zdGFuX2dsbSgNCiAgZm1sLA0KICBkYXRhID0gdHJhaW5zZXQsIA0KICBmYW1pbHkgPSBiaW5vbWlhbChsaW5rID0gImxvZ2l0IiksIA0KICBwcmlvciA9IHRfcHJpb3IsDQogIHByaW9yX2ludGVyY2VwdCA9IHRfcHJpb3IsDQogIGFsZ29yaXRobT0ic2FtcGxpbmciLA0KICBjaGFpbnMgPSAyLCBpdGVyID0gMTUwMCx3YXJtdXAgPSA1MDAsDQogIGNvcmVzPXBhcmFsbGVsOjpkZXRlY3RDb3JlcygpLA0KICBzZWVkPTEyMw0KKQ0KDQpgYGANCg0KVHLDrG5oIGLDoHkga+G6v3QgcXXhuqMgdMOzbSB04bqvdDoNCg0KYGBge3IgbWVzc2FnZSA9IEZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0Kc3VtbWFyeShmaXR0cHJpb3IsZGlnaXRzPTUscHJvYnM9YygwLjAyNSwwLjk3NSkpDQoNCnRlc3RwcmVkPXByZWRpY3QoZml0dHByaW9yLG5ld2RhdGE9dGVzdHNldCx0eXBlPSJsaW5rIiklPiVleHAoKQ0KdGVzdHNldCRUcnV0aD1pZmVsc2UodGVzdHNldCRDbGFzcz09MSwiTWFsaWduYW50IiwiQmVuaWduIikNCnRlc3RzZXQkUHJlZD10ZXN0cHJlZA0KdGVzdHNldCRQcmVkPWlmZWxzZSh0ZXN0c2V0JFByZWQ+PTAuNSwiTWFsaWduYW50IiwiQmVuaWduIikNCg0KY2FyZXQ6OmNvbmZ1c2lvbk1hdHJpeCh0ZXN0c2V0JFByZWQscmVmZXJlbmNlPXRlc3RzZXQkVHJ1dGgsbW9kZT0iZXZlcnl0aGluZyIscG9zaXRpdmU9Ik1hbGlnbmFudCIpDQpgYGANCg0KDQojIEJheWVzIGNsYXNzaWZpY2F0aW9uIHbhu5tpIG5haXZlQmF5ZXMoKQ0KDQrEkOG6v24gxJHDonkgY2jDum5nIHRhIGxvYWQgcGFja2FnZSBlMTA3MSDEkeG7gyBjaOG6oXkgaMOgbSBuYWl2ZUJheWVzKCkgY2hvIGPDuW5nIG3hu5l0IG3DtCBow6xuaCBuaMawIHRyw6puLiBQcmVkMiBsw6AgcHJlZGljdGlvbnMgY2hvIHRlc3QgZGF0YXNldC4gSMOjeSBuaMOsbiB2w6BvIGvhur90IHF14bqjIHBow6JuIHTDrWNoIGNvbmZ1c2lvbiBtYXRyaXggY+G7p2EgUHJlZDIgxJHhu4Mgc28gc8OhbmggduG7m2kga+G6v3QgcXXhuqMgcGjDom4gdMOtY2ggduG7m2kgaMOgbSBzdGFuX2dsbSgpLg0KDQpgYGB7ciBtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoZTEwNzEpDQoNCmZtbDIgPC0gQ2xhc3MgfiBjbHVtcHRoaWNrbmVzcyArIFNpemVVbmlmb3JtaXR5ICsgU2hhcGVVbmlmb3JtaXR5ICsgTWFyZ2luX2FkaGVzaW9uICsNCiAgRXBpQ2VsbFNpemUgKyBCYXJlbnVjbGVpICsgQmxhbmRDaHJvbWF0aW4gKyBOb3JtYWxOdWNsZW9saSArIE1pdG9zZXMNCg0KZml0IDwtIG5haXZlQmF5ZXMoZm1sMiwgZGF0YSA9IHRyYWluc2V0LCBsYXBsYWNlID0gMCwgbmEuYWN0aW9uID0gbmEucGFzcykNCg0KIyB0cmFpbnNldA0KcHJlZDEgPC0gcHJlZGljdChmaXQsIG5ld2RhdGEgPSB0cmFpbnNldCwgdHlwZSA9ICJjbGFzcyIgKQ0KdGFibGUocHJlZDEsdHJhaW5zZXQkQ2xhc3MpDQpjb25mdXNpb25NYXRyaXgocHJlZDEsIHRyYWluc2V0JENsYXNzLCAgcG9zaXRpdmUgPSAiMSIpDQoNCiN0ZXN0c2V0DQpwcmVkMiA8LSBwcmVkaWN0KGZpdCwgbmV3ZGF0YSA9IHRlc3RzZXQsIHR5cGUgPSAiY2xhc3MiICkNCnRhYmxlKHByZWQyLHRlc3RzZXQkQ2xhc3MpDQpjb25mdXNpb25NYXRyaXgocHJlZDIsIHRlc3RzZXQkQ2xhc3MsICBwb3NpdGl2ZSA9ICIxIikNCg0KYGBgDQoNCkFjY3VyYWN5IGzDoCAwLjk2MTcgc28gduG7m2kgMC45OTQ1IGPhu6dhIGjDoG0gc3Rhbl9nbG0gdsOgIFNlbnNpdGl2aXR5IGPFqW5nIG5oxrAgU3BlY2lmaWNpdHkgxJHhu4F1IHRo4bqlcCBoxqFuIG3hu5l0IMOtdCBzbyB24bubaSBr4bq/dCBxdeG6oyBwaMOibiB0w61jaCBi4bqxbmcgaMOgbSBzdGFuX2dsbSgpLg0KDQoNCiMgUk9DIGNobyBoYWkgbcO0IGjDrG5oIHBow6JuIHTDrWNoDQoNClJPQyBjaG8gbcO0IGjDrG5oIHbhu5tpIG5haXZlQmF5ZXMNCg0KYGBge3IgbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KFJPQ1IpDQpsaWJyYXJ5KGdncGxvdDIpDQojIFJPQyBmb3IgbmFpdmVCYXllcw0KeFRlc3QgPC0gdGVzdHNldFssLTEwXQ0KcHJvYnMgPC0gcHJlZGljdChmaXQsIHhUZXN0LCB0eXBlPSJyYXciKQ0KcHJlZDMgPC0gcHJlZGljdGlvbihwcm9ic1ssICIxIl0sIHRlc3RzZXQkQ2xhc3MpDQpwZXJmX25iIDwtIHBlcmZvcm1hbmNlKHByZWQzLCBtZWFzdXJlPSd0cHInLCB4Lm1lYXN1cmU9J2ZwcicpDQpwbG90KHBlcmZfbmIpDQpwZXJmb3JtYW5jZShwcmVkMywgJ2F1YycpDQoNCmBgYA0KDQpST0MgY2hvIG3DtCBow6xuaCB24bubaSBCYXllcyBMb2dpc3RpYyBSZWdyZXNzaW9uOg0KDQpgYGB7ciBtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCiMgUk9DIGZvciBsb2dpc3RpYyBCYXllcw0KcHJlZCA8LSBwcmVkaWN0aW9uKHRlc3RwcmVkLCB0ZXN0c2V0JENsYXNzKQ0KcGVyZl9sciA8LSBwZXJmb3JtYW5jZShwcmVkLCBtZWFzdXJlPSd0cHInLCB4Lm1lYXN1cmU9J2ZwcicpDQpwbG90KHBlcmZfbHIpDQoNCmBgYA0KDQpTbyBzw6FuaCBoYWkgxJHGsOG7nW5nIGNvbmcgUk9DIGNobyBoYWkgbcO0IGjDrG5oIOG7nyB0csOqbjogxJDGsOG7nW5nIGNvbmcgUk9DIGPhu6dhIG3DtCBow6xuaCBiYXllcyB24bubaSBow6BtIHN0YW5fZ2xtKCkgY2hvIHRo4bqleSBjw7MgbeG7qWMgxJHhu5kgY2jDrW5oIHjDoWMgY2FvIGjGoW4uDQoNCmBgYHtyIG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0Kcm9jX25iIDwtIGRhdGEuZnJhbWUoZnByPXVubGlzdChwZXJmX25iQHgudmFsdWVzKSwgdHByPXVubGlzdChwZXJmX25iQHkudmFsdWVzKSkNCnJvY19uYiRtZXRob2QgPC0gIm5haXZlIEJheWVzIg0Kcm9jX2xyIDwtIGRhdGEuZnJhbWUoZnByPXVubGlzdChwZXJmX2xyQHgudmFsdWVzKSwgdHByPXVubGlzdChwZXJmX2xyQHkudmFsdWVzKSkNCnJvY19sciRtZXRob2QgPC0gIkJheWVzIGxvZ2lzdGljIHJlZ3Jlc3Npb24iDQpyYmluZChyb2NfbmIsIHJvY19scikgJT4lDQogIGdncGxvdChkYXRhPS4sIGFlcyh4PWZwciwgeT10cHIsIGxpbmV0eXBlPW1ldGhvZCwgY29sb3I9bWV0aG9kKSkgKyANCiAgZ2VvbV9saW5lKCkgKw0KICBnZW9tX2FibGluZShhPTEsIGI9MCwgbGluZXR5cGU9MikgKw0KICBzY2FsZV94X2NvbnRpbnVvdXMobGltPWMoMCwxKSkgKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGltPWMoMCwxKSkgKw0KICB0aGVtZShsZWdlbmQucG9zaXRpb249YygwLjgsMC4yKSwgbGVnZW5kLnRpdGxlPWVsZW1lbnRfYmxhbmsoKSkNCmBgYA0KDQpWaeG7h2MgcGjDom4gdMOtY2ggdsOgIHNvIHPDoW5oIGhhaSBtw7QgaMOsbmggY2xhc3NpZmljYXRpb24gdGhlbyB0csaw4budbmcgcGjDoWkgQmF5ZXMgbmjGsG5nIHThu6sgaGFpIHBhY2thZ2Uga2jDoWMgbmhhdSDEkeG7gyBt4bufIHLhu5luZyBz4buxIGhp4buBdSBiaeG6v3QgY+G7p2EgY2jDum5nIHRhIHbhu4EgY8OhYyBow6BtIGtow6FjIG5oYXUgduG7gSBwaMawxqFuZyBwaMOhcCBwaMOibiB0w61jaC4gQ2jDum5nIHRhIHRo4bqleSBwaMawxqFuZyBwaMOhcCBOYWl2ZSBCYXllcyBjbGFzc2lmaWNhdGlvbiB0aMaw4budbmcgY2hvIGvhur90IHF14bqjIHBow6JuIGxv4bqhaSB24bubaSBt4bupYyDEkeG7mSBjaMOtbmggeMOhYyB0aOG6pXAgaMahbiBt4buZdCDDrXQgc28gduG7m2kgcGjGsMahbmcgcGjDoXAgTG9naXN0aWMgUmVncmVzc2lvbiwgdHV5IHLhurFuZyBjw7luZyB0csaw4budbmcgcGjDoWkgQmF5ZXMuDQoNClLhuqV0IG1vbmcgbmjhuq1uIMSRxrDhu6NjIMO9IGtp4bq/biBj4bunYSBjw6FjIGFuaCBjaOG7iywgY8OhYyBi4bqhbiB24buBIHPhu7Egc28gc8OhbmggdHLDqm4gxJHhu4MgaGnhu4N1IHLDtSBoxqFuIHbhu4EgY2jhu6cgxJHhu4EgbsOgeSB2w6AgY8OhbSDGoW4gbeG7jWkgbmfGsOG7nWkgxJHDoyBkw6BuaCB0aOG7nWkgZ2lhbiDEkeG7jWMgYsOgaSB2aeG6v3QuDQo=