1 Giới thiệu

Câu chuyện về mô hình hồi quy logistic đã được lặp lại khá nhiều lần trong quá khứ, tuy nhiên trong bài thực hành ngắn này Nhi muốn trình bày 2 biến tấu của loại mô hình này với bản chất khác nhau, nhưng cả 2 đều liên quan đến Tensorflow.

2 Minh họa : phân biệt u vú lành tính hay ác tính

Nhi dùng lại thí dụ minh họa như trong bài Bayes hồi quy logistic lần trước với dataset Biopsy trong package MASS. Dataset này bao gồm 9 chỉ số về đặc tính giải phẫu bệnh lý của mô sinh thiết ở bệnh nhân có khối u Vú, và 1 biến định tính nhị phân là phân loại Lành tính hay ác tính của khối u.

Nếu đặt phân loại Lành/Ác tính như biến kết quả Y được mã hóa bằng 2 con số 0 (lành tính) và 1 (ác tính), việc dựng một mô hình hồi quy cho phép trả lời cả 2 câu hỏi tiên lượng và diễn giải quan hệ giữa các biến.

library(tidyverse)
## -- Attaching packages --------------------------------------- tidyverse 1.2.1 --
## v ggplot2 3.0.0     v purrr   0.2.5
## v tibble  1.4.2     v dplyr   0.7.6
## v tidyr   0.8.1     v stringr 1.3.1
## v readr   1.1.1     v forcats 0.3.0
## -- Conflicts ------------------------------------------ tidyverse_conflicts() --
## x dplyr::filter() masks stats::filter()
## x dplyr::lag()    masks stats::lag()
df=read.csv("http://vincentarelbundock.github.io/Rdatasets/csv/MASS/biopsy.csv")%>%as_tibble()%>%.[,c(3:12)]%>%na.omit()

names(df)=c("clumpthickness",
            "SizeUniformity",
            "ShapeUniformity",
            "Margin_adhesion",
            "EpiCellSize",
            "Barenuclei",
            "BlandChromatin",
            "NormalNucleoli",
            "Mitoses",
            "Class"
)

df<-df%>%mutate(.,Class=as.integer(.$Class)-1L)

head(df)

3 Hàm glm()

Một mô hình logistic có thể được thực hiện đơn giản bằng hàm glm()

glm_mod<-glm(as.factor(Class)~.,data=df,family=stats::binomial())

summary(glm_mod)
## 
## Call:
## glm(formula = as.factor(Class) ~ ., family = stats::binomial(), 
##     data = df)
## 
## Deviance Residuals: 
##     Min       1Q   Median       3Q      Max  
## -3.4841  -0.1153  -0.0619   0.0222   2.4698  
## 
## Coefficients:
##                  Estimate Std. Error z value Pr(>|z|)    
## (Intercept)     -10.10394    1.17488  -8.600  < 2e-16 ***
## clumpthickness    0.53501    0.14202   3.767 0.000165 ***
## SizeUniformity   -0.00628    0.20908  -0.030 0.976039    
## ShapeUniformity   0.32271    0.23060   1.399 0.161688    
## Margin_adhesion   0.33064    0.12345   2.678 0.007400 ** 
## EpiCellSize       0.09663    0.15659   0.617 0.537159    
## Barenuclei        0.38303    0.09384   4.082 4.47e-05 ***
## BlandChromatin    0.44719    0.17138   2.609 0.009073 ** 
## NormalNucleoli    0.21303    0.11287   1.887 0.059115 .  
## Mitoses           0.53484    0.32877   1.627 0.103788    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 884.35  on 682  degrees of freedom
## Residual deviance: 102.89  on 673  degrees of freedom
## AIC: 122.89
## 
## Number of Fisher Scoring iterations: 8
library(broom)

glm_mod%>%tidy()%>%mutate(OR=exp(estimate),
                           UL=exp(estimate+1.96*std.error),
                           LL=exp(estimate-1.96*std.error))%>%
  select(estimate,OR,LL,UL,p.value)%>%round(.,4)

4 Logistic như mạng neuron 1 lớp

Tuy nhiên, chúng ta cũng có thể xem hồi quy logistic là một trường hợp đơn giản nhất của mạng Neuron (Neural network), chỉ có 1 lớp neuron duy nhất, được kích hoạt bằng 1 hàm sigmoid và nối với 1 neuron xuất kết quả.

Ta sẽ thử dựng neural network này với keras và TensorFlow backend, cấu trúc của mạng neuron gồm 1 lớp duy nhất để tiếp nhận 1 Intercept và 9 biến, kích hoạt bằng hàm sigmoid, xuất ra 1 neuron kết quả. Mô hình được huấn luyện với hàm loss là binary_crossentropy và tiêu chí kiểm định là accuracy

library(keras)

build_model <- function(){
  model_keras <- keras_model_sequential()%>% 
    layer_dense(
      units              = 1, 
      kernel_initializer = "uniform",
      activation         = "sigmoid", 
      input_shape        = 10)%>% 
    compile(
      optimizer = 'sgd',
      loss      = 'binary_crossentropy',
      metrics   = c('binary_accuracy')
    )
}
design_mat<-model.matrix(~., data=df[,-10])

head(design_mat,5)
##   (Intercept) clumpthickness SizeUniformity ShapeUniformity
## 1           1              5              1               1
## 2           1              5              4               4
## 3           1              3              1               1
## 4           1              6              8               8
## 5           1              4              1               1
##   Margin_adhesion EpiCellSize Barenuclei BlandChromatin NormalNucleoli
## 1               1           2          1              3              1
## 2               5           7         10              3              2
## 3               1           2          2              3              1
## 4               1           3          4              3              7
## 5               3           2          1              3              1
##   Mitoses
## 1       1
## 2       1
## 3       1
## 4       1
## 5       1

Với 20 batchs dữ liệu và 100 lượt train, mô hình nhanh chóng đạt trạng thái bão hòa với Accuracy = 0.99

model<-build_model()

model
## Model
## ___________________________________________________________________________
## Layer (type)                     Output Shape                  Param #     
## ===========================================================================
## dense_1 (Dense)                  (None, 1)                     11          
## ===========================================================================
## Total params: 11
## Trainable params: 11
## Non-trainable params: 0
## ___________________________________________________________________________
fit_keras <- fit(
  object=model,
  x=design_mat, 
  y=df$Class,
  batch_size = 20,
  epochs= 100,
  validation_split = 0.3
)

fit_keras
## Trained on 478 samples, validated on 205 samples (batch_size=20, epochs=100)
## Final epoch (plot to see history):
##            val_loss: 0.108
## val_binary_accuracy: 0.9951
##                loss: 0.1709
##     binary_accuracy: 0.9477

Những giá trị trọng số cho lớp neuron trong mô hình:

get_weights(model)%>%.[[1]]->wdf

rownames(wdf)<-colnames(design_mat)

wdf
##                        [,1]
## (Intercept)     -1.67289507
## clumpthickness   0.08638906
## SizeUniformity   0.32727787
## ShapeUniformity  0.19069567
## Margin_adhesion  0.11432683
## EpiCellSize     -0.18441433
## Barenuclei       0.36628929
## BlandChromatin  -0.20194633
## NormalNucleoli   0.20442222
## Mitoses          0.03903888

5 Logistic theo Bayes

Tiếp theo, ta sẽ dùng công cụ “greta” đã giới thiệu ở bài trước để dựng 2 mô hình logistic theo phái Bayes, một dùng quy luật phân bố nhị thức (Binomial), một dùng họ phân phối Bernoulli.

Trong bài trước, Nhi đã giới thiệu về greta, thế hệ ngôn ngữ Bayes thứ tư và sử dụng Tensorflow. Thí dụ minh họa trong bài là một mô hình ANOVA, sử dụng design matrix làm dữ liệu đầu vào và likelihood là phân phối Gaussian. Cho bài toán mô hình logistic, chúng ta cùng đi ngược từ kết quả: Class là 1 biến nhị phân với giá trị 0/1. Để ước lượng Class bằng một mô hình hồi quy tuyến tính, ta phải tìm cách ánh xạ giá trị của biến kết quả Y trong mô hình trên khoảng 0-1 ; hai quy luật phân bố có thể áp dụng cho hàm likelihood của Y đó là binomial :

\[ Y \sim Binomial (N,p)\]

và Bernoulli

\[ Y \sim Bernoulli(p)\]

Trong đó, p lsinh ra từ hàm liên kết logit cho kết quả của một mô hình hồi quy tuyến tính. Mục tiêu của chúng ta là xác định 9 tham số beta và Intercept của mô hình.

Giả thuyết tiền định về beta và intercept, đó là chúng có phân bố chuẩn, với mu=0, và sigma = 10

5.1 Mô hình sử dụng Binomial (n,p)

Ta viết mô hình đầu tiên sử dụng likelihood là phân phối binomial(n,p)

library(greta)
## 
## Attaching package: 'greta'
## The following object is masked _by_ '.GlobalEnv':
## 
##     model
## The following objects are masked from 'package:stats':
## 
##     binomial, poisson
## The following objects are masked from 'package:base':
## 
##     %*%, backsolve, beta, colMeans, colSums, diag, eigen,
##     forwardsolve, gamma, rowMeans, rowSums, sweep, tapply
# create matrices to greta arrays
features <- as_data(df[,-10])

Y <- as_data(df$Class)

# create greta arrays for random variables
intercept <- normal(0, 10)
beta <- normal(0, 10, dim = 9L)

# logit-linear model
linear_predictor <- intercept + features %*% beta

p <- ilogit(linear_predictor)

# distribution (likelihood) over observed values
distribution(Y) <- binomial(nrow(df),p)

binom_mod <- greta::model(intercept,beta)
plot(binom_mod)

Sau đó ta sampling cho 10 chuỗi MCMC

draws <- mcmc(binom_mod,
              warmup = 1000,
              n_samples = 5000,             
              pb_update = 100)

Các chuỗi MCMC được thực hiện rất nhanh chóng, ta có thể xuất kết quả của mô hình

summary(draws)
## 
## Iterations = 1:5000
## Thinning interval = 1 
## Number of chains = 1 
## Sample size per chain = 5000 
## 
## 1. Empirical mean and standard deviation for each variable,
##    plus standard error of the mean:
## 
##                Mean      SD  Naive SE Time-series SE
## intercept -10.00825 0.21935 0.0031020      0.0038928
## beta[1,1]   0.12156 0.02870 0.0004058      0.0007045
## beta[2,1]   0.05680 0.04318 0.0006107      0.0030992
## beta[3,1]   0.02538 0.04411 0.0006239      0.0035118
## beta[4,1]   0.01947 0.02461 0.0003481      0.0014159
## beta[5,1]   0.02885 0.03119 0.0004411      0.0007063
## beta[6,1]   0.12853 0.02340 0.0003310      0.0007970
## beta[7,1]   0.03810 0.03494 0.0004942      0.0011135
## beta[8,1]   0.04888 0.02252 0.0003185      0.0008728
## beta[9,1]  -0.02137 0.02816 0.0003982      0.0006779
## 
## 2. Quantiles for each variable:
## 
##                2.5%        25%       50%       75%    97.5%
## intercept -10.44277 -10.155088 -10.00590 -9.860210 -9.59030
## beta[1,1]   0.06550   0.102452   0.12148  0.140905  0.17798
## beta[2,1]  -0.02692   0.027700   0.05702  0.085334  0.14238
## beta[3,1]  -0.06406  -0.004164   0.02385  0.056799  0.11109
## beta[4,1]  -0.02916   0.002400   0.01996  0.037015  0.06578
## beta[5,1]  -0.03158   0.008081   0.02859  0.049745  0.09144
## beta[6,1]   0.08371   0.112772   0.12851  0.143577  0.17471
## beta[7,1]  -0.03078   0.015994   0.03796  0.061085  0.10775
## beta[8,1]   0.00629   0.033133   0.04811  0.064587  0.09267
## beta[9,1]  -0.07776  -0.040327  -0.02124 -0.001916  0.03364

Cũng như phân phối hậu nghiệm của 9 Odds-ratios

post_df<-draws[[1]]%>%
  as.data.frame()%>%.[,c(1:10)]%>%
  mutate(iteration=c(1:5000))

colnames(post_df)=c("Intercept",colnames(df[,-10]),"Iteration")

post_df%>%gather(colnames(df[,-10]),key="Para",value="Value")%>%
  mutate(OR=exp(Value))%>%
  ggplot(aes(x=OR))+
  geom_density(alpha=0.5,aes(fill=Para,col=Para),show.legend = F)+
  theme_bw()+
  geom_vline(xintercept=1,col="blue4",linetype=2)+
  facet_wrap(~Para,ncol=3,scales="free")

library(ggridges)
## 
## Attaching package: 'ggridges'
## The following object is masked from 'package:ggplot2':
## 
##     scale_discrete_manual
library(viridis)
## Loading required package: viridisLite
post_df%>%gather(colnames(df[,-10]),key="Para",value="Value")%>%
  mutate(OR=exp(Value))%>%
  ggplot()+
  geom_density_ridges_gradient(aes(x=OR,y=reorder(Para,OR),fill=..x..),
                               scale=1,alpha=0.01,
                               show.legend =F)+
  scale_fill_viridis(option="C",direction = -1)+  
  labs(x="Odds-ratio",y="Betas")+
  geom_vline(xintercept = 1,col="blue",linetype=2)+
  theme_bw()
## Picking joint bandwidth of 0.00533

5.2 Mô hình sử dụng Bernoulli(p)

# create matrices to greta arrays
features <- as_data(df[,-10])

Y <- as_data(df$Class)

# create greta arrays for random variables
intercept <- normal(0, 10)
beta <- normal(0, 10, dim = 9L)

# logit-linear model
linear_predictor <- intercept + features %*% beta

prob <- ilogit(linear_predictor)

# distribution (likelihood) over observed values
distribution(Y) <- bernoulli(prob)

mod_bernoulli <- greta::model(intercept,beta)

plot(mod_bernoulli)

Ta làm tương tự như trên để có 10 chuỗi MCMC và xuất kết quả

drawsB <- mcmc(mod_bernoulli,
              warmup = 2000,
              n_samples = 5000,             
              pb_update = 100)

post_df2<-drawsB[[1]]%>%as.data.frame()%>%mutate(iteration=c(1:5000))

colnames(post_df2)=c("Intercept",colnames(df[,-10]),"Iteration")


post_df2%>%gather(colnames(df[,-10]),key="Para",value="Value")%>%
  mutate(OR=exp(Value))%>%
  ggplot(aes(x=OR))+
  geom_density(alpha=0.5,aes(fill=Para,col=Para),show.legend = F)+
  theme_bw()+
  geom_vline(xintercept=1,col="blue4",linetype=2)+
  facet_wrap(~Para,ncol=3,scales="free")

post_df2%>%gather(colnames(df[,-10]),key="Para",value="Value")%>%
  mutate(OR=exp(Value))%>%
  ggplot()+
  geom_density_ridges_gradient(aes(x=OR,y=reorder(Para,OR),fill=..x..),
                               scale=1,alpha=0.01,
                               show.legend =F)+
  scale_fill_viridis(option="D",direction = -1)+  
  labs(x="Odds-ratio",y="Betas")+
  geom_vline(xintercept = 1,col="blue",linetype=2)+
  theme_bw()
## Picking joint bandwidth of 0.0326

6 Tổng kết

Chúng ta vừa thực hiện 2 biến thể của mô hình hồi quy logistic, đó là mạng neuron 1 lớp và hồi quy Bayes. Tuy khác nhau về cơ chế, cả 2 đều dựa vào Tensorflow. Hy vọng bài viết giúp các bạn mua vui được vài phút.

Cuối tuần vui vẻ …

LS0tDQp0aXRsZTogIkjhu5NpIHF1eSBCYXllcyBi4bqxbmcgVGVuc29yRmxvdyINCmF1dGhvcjogIkzDqiBOZ+G7jWMgS2jhuqMgTmhpIg0KZGF0ZTogIjI0IFRow6FuZyA3IDIwMTgiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGRmX3ByaW50OiBwYWdlZA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHRydWUNCiAgICB0aGVtZTogImRlZmF1bHQiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQogICAgdG9jX2RlcHRoOiAzDQogICAgZGV2OiAnc3ZnJw0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpDQpgYGANCg0KIVtdKGdyZXRhbG9naXN0aWMucG5nKQ0KDQojIEdp4bubaSB0aGnhu4d1DQoNCkPDonUgY2h1eeG7h24gduG7gSBtw7QgaMOsbmggaOG7k2kgcXV5IGxvZ2lzdGljIMSRw6MgxJHGsOG7o2MgbOG6t3AgbOG6oWkga2jDoSBuaGnhu4F1IGzhuqduIHRyb25nIHF1w6Ega2jhu6ksIHR1eSBuaGnDqm4gdHJvbmcgYsOgaSB0aOG7sWMgaMOgbmggbmfhuq9uIG7DoHkgTmhpIG114buRbiB0csOsbmggYsOgeSAyIGJp4bq/biB04bqldSBj4bunYSBsb+G6oWkgbcO0IGjDrG5oIG7DoHkgduG7m2kgYuG6o24gY2jhuqV0IGtow6FjIG5oYXUsIG5oxrBuZyBj4bqjIDIgxJHhu4F1IGxpw6puIHF1YW4gxJHhur9uIFRlbnNvcmZsb3cuDQoNCiMgTWluaCBo4buNYSA6IHBow6JuIGJp4buHdCB1IHbDuiBsw6BuaCB0w61uaCBoYXkgw6FjIHTDrW5oDQoNCk5oaSBkw7luZyBs4bqhaSB0aMOtIGThu6UgbWluaCBo4buNYSBuaMawIHRyb25nIGLDoGkgQmF5ZXMgaOG7k2kgcXV5IGxvZ2lzdGljIGzhuqduIHRyxrDhu5tjIHbhu5tpIGRhdGFzZXQgQmlvcHN5IHRyb25nIHBhY2thZ2UgTUFTUy4gRGF0YXNldCBuw6B5IGJhbyBn4buTbSA5IGNo4buJIHPhu5EgduG7gSDEkeG6t2MgdMOtbmggZ2nhuqNpIHBo4bqrdSBi4buHbmggbMO9IGPhu6dhIG3DtCBzaW5oIHRoaeG6v3Qg4bufIGLhu4duaCBuaMOibiBjw7Mga2jhu5FpIHUgVsO6LCB2w6AgMSBiaeG6v24gxJHhu4tuaCB0w61uaCBuaOG7iyBwaMOibiBsw6AgcGjDom4gbG/huqFpIEzDoG5oIHTDrW5oIGhheSDDoWMgdMOtbmggY+G7p2Ega2jhu5FpIHUuDQoNCk7hur91IMSR4bq3dCBwaMOibiBsb+G6oWkgTMOgbmgvw4FjIHTDrW5oIG5oxrAgYmnhur9uIGvhur90IHF14bqjIFkgxJHGsOG7o2MgbcOjIGjDs2EgYuG6sW5nIDIgY29uIHPhu5EgMCAobMOgbmggdMOtbmgpIHbDoCAxICjDoWMgdMOtbmgpLCB2aeG7h2MgZOG7sW5nIG3hu5l0IG3DtCBow6xuaCBo4buTaSBxdXkgY2hvIHBow6lwIHRy4bqjIGzhu51pIGPhuqMgMiBjw6J1IGjhu49pIHRpw6puIGzGsOG7o25nIHbDoCBkaeG7hW4gZ2nhuqNpIHF1YW4gaOG7hyBnaeG7r2EgY8OhYyBiaeG6v24uDQoNCg0KIVtdKGxvZ2lzdGljdmlld3MucG5nKQ0KDQpgYGB7cn0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KDQpkZj1yZWFkLmNzdigiaHR0cDovL3ZpbmNlbnRhcmVsYnVuZG9jay5naXRodWIuaW8vUmRhdGFzZXRzL2Nzdi9NQVNTL2Jpb3BzeS5jc3YiKSU+JWFzX3RpYmJsZSgpJT4lLlssYygzOjEyKV0lPiVuYS5vbWl0KCkNCg0KbmFtZXMoZGYpPWMoImNsdW1wdGhpY2tuZXNzIiwNCiAgICAgICAgICAgICJTaXplVW5pZm9ybWl0eSIsDQogICAgICAgICAgICAiU2hhcGVVbmlmb3JtaXR5IiwNCiAgICAgICAgICAgICJNYXJnaW5fYWRoZXNpb24iLA0KICAgICAgICAgICAgIkVwaUNlbGxTaXplIiwNCiAgICAgICAgICAgICJCYXJlbnVjbGVpIiwNCiAgICAgICAgICAgICJCbGFuZENocm9tYXRpbiIsDQogICAgICAgICAgICAiTm9ybWFsTnVjbGVvbGkiLA0KICAgICAgICAgICAgIk1pdG9zZXMiLA0KICAgICAgICAgICAgIkNsYXNzIg0KKQ0KDQpkZjwtZGYlPiVtdXRhdGUoLixDbGFzcz1hcy5pbnRlZ2VyKC4kQ2xhc3MpLTFMKQ0KDQpoZWFkKGRmKQ0KYGBgDQoNCiMgSMOgbSBnbG0oKQ0KDQpN4buZdCBtw7QgaMOsbmggbG9naXN0aWMgY8OzIHRo4buDIMSRxrDhu6NjIHRo4buxYyBoaeG7h24gxJHGoW4gZ2nhuqNuIGLhurFuZyBow6BtIGdsbSgpDQoNCmBgYHtyfQ0KZ2xtX21vZDwtZ2xtKGFzLmZhY3RvcihDbGFzcyl+LixkYXRhPWRmLGZhbWlseT1zdGF0czo6Ymlub21pYWwoKSkNCg0Kc3VtbWFyeShnbG1fbW9kKQ0KDQpsaWJyYXJ5KGJyb29tKQ0KDQpnbG1fbW9kJT4ldGlkeSgpJT4lbXV0YXRlKE9SPWV4cChlc3RpbWF0ZSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBVTD1leHAoZXN0aW1hdGUrMS45NipzdGQuZXJyb3IpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgTEw9ZXhwKGVzdGltYXRlLTEuOTYqc3RkLmVycm9yKSklPiUNCiAgc2VsZWN0KGVzdGltYXRlLE9SLExMLFVMLHAudmFsdWUpJT4lcm91bmQoLiw0KQ0KDQpgYGANCg0KIyBMb2dpc3RpYyBuaMawIG3huqFuZyBuZXVyb24gMSBs4bubcA0KDQpUdXkgbmhpw6puLCBjaMO6bmcgdGEgY8WpbmcgY8OzIHRo4buDIHhlbSBo4buTaSBxdXkgbG9naXN0aWMgbMOgIG3hu5l0IHRyxrDhu51uZyBo4bujcCDEkcahbiBnaeG6o24gbmjhuqV0IGPhu6dhIG3huqFuZyBOZXVyb24gKE5ldXJhbCBuZXR3b3JrKSwgY2jhu4kgY8OzIDEgbOG7m3AgbmV1cm9uIGR1eSBuaOG6pXQsIMSRxrDhu6NjIGvDrWNoIGhv4bqhdCBi4bqxbmcgMSBow6BtIHNpZ21vaWQgdsOgIG7hu5FpIHbhu5tpIDEgbmV1cm9uIHh14bqldCBr4bq/dCBxdeG6oy4NCg0KDQohW10obG9naXN0aWNubi5wbmcpDQoNClRhIHPhur0gdGjhu60gZOG7sW5nIG5ldXJhbCBuZXR3b3JrIG7DoHkgduG7m2kga2VyYXMgdsOgIFRlbnNvckZsb3cgYmFja2VuZCwgY+G6pXUgdHLDumMgY+G7p2EgbeG6oW5nIG5ldXJvbiBn4buTbSAxIGzhu5twIGR1eSBuaOG6pXQgxJHhu4MgdGnhur9wIG5o4bqtbiAxIEludGVyY2VwdCB2w6AgOSBiaeG6v24sIGvDrWNoIGhv4bqhdCBi4bqxbmcgaMOgbSBzaWdtb2lkLCB4deG6pXQgcmEgMSBuZXVyb24ga+G6v3QgcXXhuqMuIE3DtCBow6xuaCDEkcaw4bujYyBodeG6pW4gbHV54buHbiB24bubaSBow6BtIGxvc3MgbMOgIGJpbmFyeV9jcm9zc2VudHJvcHkgdsOgIHRpw6p1IGNow60ga2nhu4NtIMSR4buLbmggbMOgIGFjY3VyYWN5IA0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCg0KbGlicmFyeShrZXJhcykNCg0KYnVpbGRfbW9kZWwgPC0gZnVuY3Rpb24oKXsNCiAgbW9kZWxfa2VyYXMgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpJT4lIA0KICAgIGxheWVyX2RlbnNlKA0KICAgICAgdW5pdHMgICAgICAgICAgICAgID0gMSwgDQogICAgICBrZXJuZWxfaW5pdGlhbGl6ZXIgPSAidW5pZm9ybSIsDQogICAgICBhY3RpdmF0aW9uICAgICAgICAgPSAic2lnbW9pZCIsIA0KICAgICAgaW5wdXRfc2hhcGUgICAgICAgID0gMTApJT4lIA0KICAgIGNvbXBpbGUoDQogICAgICBvcHRpbWl6ZXIgPSAnc2dkJywNCiAgICAgIGxvc3MgICAgICA9ICdiaW5hcnlfY3Jvc3NlbnRyb3B5JywNCiAgICAgIG1ldHJpY3MgICA9IGMoJ2JpbmFyeV9hY2N1cmFjeScpDQogICAgKQ0KfQ0KYGBgDQoNCmBgYHtyfQ0KZGVzaWduX21hdDwtbW9kZWwubWF0cml4KH4uLCBkYXRhPWRmWywtMTBdKQ0KDQpoZWFkKGRlc2lnbl9tYXQsNSkNCg0KYGBgDQoNClbhu5tpIDIwIGJhdGNocyBk4buvIGxp4buHdSB2w6AgMTAwIGzGsOG7o3QgdHJhaW4sIG3DtCBow6xuaCBuaGFuaCBjaMOzbmcgxJHhuqF0IHRy4bqhbmcgdGjDoWkgYsOjbyBow7JhIHbhu5tpIEFjY3VyYWN5ID0gMC45OQ0KDQpgYGB7cn0NCm1vZGVsPC1idWlsZF9tb2RlbCgpDQoNCm1vZGVsDQoNCmZpdF9rZXJhcyA8LSBmaXQoDQogIG9iamVjdD1tb2RlbCwNCiAgeD1kZXNpZ25fbWF0LCANCiAgeT1kZiRDbGFzcywNCiAgYmF0Y2hfc2l6ZSA9IDIwLA0KICBlcG9jaHM9IDEwMCwNCiAgdmFsaWRhdGlvbl9zcGxpdCA9IDAuMw0KKQ0KDQpmaXRfa2VyYXMNCmBgYA0KDQohW10oa2VyYXMxLnBuZykNCg0KTmjhu69uZyBnacOhIHRy4buLIHRy4buNbmcgc+G7kSBjaG8gbOG7m3AgbmV1cm9uIHRyb25nIG3DtCBow6xuaDoNCg0KYGBge3J9DQpnZXRfd2VpZ2h0cyhtb2RlbCklPiUuW1sxXV0tPndkZg0KDQpyb3duYW1lcyh3ZGYpPC1jb2xuYW1lcyhkZXNpZ25fbWF0KQ0KDQp3ZGYNCmBgYA0KDQojIExvZ2lzdGljIHRoZW8gQmF5ZXMNCg0KVGnhur9wIHRoZW8sIHRhIHPhur0gZMO5bmcgY8O0bmcgY+G7pSAiZ3JldGEiIMSRw6MgZ2nhu5tpIHRoaeG7h3Ug4bufIGLDoGkgdHLGsOG7m2MgxJHhu4MgZOG7sW5nIDIgbcO0IGjDrG5oIGxvZ2lzdGljIHRoZW8gcGjDoWkgQmF5ZXMsIG3hu5l0IGTDuW5nIHF1eSBsdeG6rXQgcGjDom4gYuG7kSBuaOG7iyB0aOG7qWMgKEJpbm9taWFsKSwgbeG7mXQgZMO5bmcgaOG7jSBwaMOibiBwaOG7kWkgQmVybm91bGxpLg0KDQpUcm9uZyBiw6BpIHRyxrDhu5tjLCBOaGkgxJHDoyBnaeG7m2kgdGhp4buHdSB24buBIGdyZXRhLCB0aOG6vyBo4buHIG5nw7RuIG5n4buvIEJheWVzIHRo4bupIHTGsCB2w6Agc+G7rSBk4bulbmcgVGVuc29yZmxvdy4gVGjDrSBk4bulIG1pbmggaOG7jWEgdHJvbmcgYsOgaSBsw6AgbeG7mXQgbcO0IGjDrG5oIEFOT1ZBLCBz4butIGThu6VuZyBkZXNpZ24gbWF0cml4IGzDoG0gZOG7ryBsaeG7h3UgxJHhuqd1IHbDoG8gdsOgIGxpa2VsaWhvb2QgbMOgIHBow6JuIHBo4buRaSBHYXVzc2lhbi4gDQpDaG8gYsOgaSB0b8OhbiBtw7QgaMOsbmggbG9naXN0aWMsIGNow7puZyB0YSBjw7luZyDEkWkgbmfGsOG7o2MgdOG7qyBr4bq/dCBxdeG6ozogQ2xhc3MgbMOgIDEgYmnhur9uIG5o4buLIHBow6JuIHbhu5tpIGdpw6EgdHLhu4sgMC8xLiDEkOG7gyDGsOG7m2MgbMaw4bujbmcgQ2xhc3MgYuG6sW5nIG3hu5l0IG3DtCBow6xuaCBo4buTaSBxdXkgdHV54bq/biB0w61uaCwgdGEgcGjhuqNpIHTDrG0gY8OhY2ggw6FuaCB44bqhIGdpw6EgdHLhu4sgY+G7p2EgYmnhur9uIGvhur90IHF14bqjIFkgdHJvbmcgbcO0IGjDrG5oIHRyw6puIGtob+G6o25nIDAtMSA7IGhhaSBxdXkgbHXhuq10IHBow6JuIGLhu5EgY8OzIHRo4buDIMOhcCBk4bulbmcgY2hvIGjDoG0gbGlrZWxpaG9vZCBj4bunYSBZIMSRw7MgbMOgIGJpbm9taWFsIDoNCg0KJCQgWSBcc2ltIEJpbm9taWFsIChOLHApJCQNCg0KdsOgIEJlcm5vdWxsaQ0KDQokJCBZIFxzaW0gQmVybm91bGxpKHApJCQNCg0KVHJvbmcgxJHDsywgcCBsc2luaCByYSB04burIGjDoG0gbGnDqm4ga+G6v3QgbG9naXQgY2hvIGvhur90IHF14bqjIGPhu6dhIG3hu5l0IG3DtCBow6xuaCBo4buTaSBxdXkgdHV54bq/biB0w61uaC4gTeG7pWMgdGnDqnUgY+G7p2EgY2jDum5nIHRhIGzDoCB4w6FjIMSR4buLbmggOSB0aGFtIHPhu5EgYmV0YSB2w6AgSW50ZXJjZXB0IGPhu6dhIG3DtCBow6xuaC4gDQoNCkdp4bqjIHRodXnhur90IHRp4buBbiDEkeG7i25oIHbhu4EgYmV0YSB2w6AgaW50ZXJjZXB0LCDEkcOzIGzDoCBjaMO6bmcgY8OzIHBow6JuIGLhu5EgY2h14bqpbiwgduG7m2kgbXU9MCwgdsOgIHNpZ21hID0gMTANCg0KIyMgTcO0IGjDrG5oIHPhu60gZOG7pW5nIEJpbm9taWFsIChuLHApDQoNClRhIHZp4bq/dCBtw7QgaMOsbmggxJHhuqd1IHRpw6puIHPhu60gZOG7pW5nIGxpa2VsaWhvb2QgbMOgIHBow6JuIHBo4buRaSBiaW5vbWlhbChuLHApDQoNCmBgYHtyfQ0KbGlicmFyeShncmV0YSkNCg0KIyBjcmVhdGUgbWF0cmljZXMgdG8gZ3JldGEgYXJyYXlzDQpmZWF0dXJlcyA8LSBhc19kYXRhKGRmWywtMTBdKQ0KDQpZIDwtIGFzX2RhdGEoZGYkQ2xhc3MpDQoNCiMgY3JlYXRlIGdyZXRhIGFycmF5cyBmb3IgcmFuZG9tIHZhcmlhYmxlcw0KaW50ZXJjZXB0IDwtIG5vcm1hbCgwLCAxMCkNCmJldGEgPC0gbm9ybWFsKDAsIDEwLCBkaW0gPSA5TCkNCg0KIyBsb2dpdC1saW5lYXIgbW9kZWwNCmxpbmVhcl9wcmVkaWN0b3IgPC0gaW50ZXJjZXB0ICsgZmVhdHVyZXMgJSolIGJldGENCg0KcCA8LSBpbG9naXQobGluZWFyX3ByZWRpY3RvcikNCg0KIyBkaXN0cmlidXRpb24gKGxpa2VsaWhvb2QpIG92ZXIgb2JzZXJ2ZWQgdmFsdWVzDQpkaXN0cmlidXRpb24oWSkgPC0gYmlub21pYWwobnJvdyhkZikscCkNCg0KYmlub21fbW9kIDwtIGdyZXRhOjptb2RlbChpbnRlcmNlcHQsYmV0YSkNCmBgYA0KDQpgYGB7cn0NCnBsb3QoYmlub21fbW9kKQ0KYGBgDQoNCiFbXShiaW5vbS5wbmcpDQoNClNhdSDEkcOzIHRhIHNhbXBsaW5nIGNobyAxMCBjaHXhu5dpIE1DTUMgDQoNCmBgYHtyfQ0KZHJhd3MgPC0gbWNtYyhiaW5vbV9tb2QsDQogICAgICAgICAgICAgIHdhcm11cCA9IDEwMDAsDQogICAgICAgICAgICAgIG5fc2FtcGxlcyA9IDUwMDAsICAgICAgICAgICAgIA0KICAgICAgICAgICAgICBwYl91cGRhdGUgPSAxMDApDQpgYGANCg0KQ8OhYyBjaHXhu5dpIE1DTUMgxJHGsOG7o2MgdGjhu7FjIGhp4buHbiBy4bqldCBuaGFuaCBjaMOzbmcsIHRhIGPDsyB0aOG7gyB4deG6pXQga+G6v3QgcXXhuqMgY+G7p2EgbcO0IGjDrG5oDQoNCmBgYHtyfQ0Kc3VtbWFyeShkcmF3cykNCmBgYA0KDQpDxaluZyBuaMawIHBow6JuIHBo4buRaSBo4bqtdSBuZ2hp4buHbSBj4bunYSA5IE9kZHMtcmF0aW9zDQoNCmBgYHtyfQ0KcG9zdF9kZjwtZHJhd3NbWzFdXSU+JQ0KICBhcy5kYXRhLmZyYW1lKCklPiUuWyxjKDE6MTApXSU+JQ0KICBtdXRhdGUoaXRlcmF0aW9uPWMoMTo1MDAwKSkNCg0KY29sbmFtZXMocG9zdF9kZik9YygiSW50ZXJjZXB0Iixjb2xuYW1lcyhkZlssLTEwXSksIkl0ZXJhdGlvbiIpDQoNCnBvc3RfZGYlPiVnYXRoZXIoY29sbmFtZXMoZGZbLC0xMF0pLGtleT0iUGFyYSIsdmFsdWU9IlZhbHVlIiklPiUNCiAgbXV0YXRlKE9SPWV4cChWYWx1ZSkpJT4lDQogIGdncGxvdChhZXMoeD1PUikpKw0KICBnZW9tX2RlbnNpdHkoYWxwaGE9MC41LGFlcyhmaWxsPVBhcmEsY29sPVBhcmEpLHNob3cubGVnZW5kID0gRikrDQogIHRoZW1lX2J3KCkrDQogIGdlb21fdmxpbmUoeGludGVyY2VwdD0xLGNvbD0iYmx1ZTQiLGxpbmV0eXBlPTIpKw0KICBmYWNldF93cmFwKH5QYXJhLG5jb2w9MyxzY2FsZXM9ImZyZWUiKQ0KDQpsaWJyYXJ5KGdncmlkZ2VzKQ0KbGlicmFyeSh2aXJpZGlzKQ0KDQpwb3N0X2RmJT4lZ2F0aGVyKGNvbG5hbWVzKGRmWywtMTBdKSxrZXk9IlBhcmEiLHZhbHVlPSJWYWx1ZSIpJT4lDQogIG11dGF0ZShPUj1leHAoVmFsdWUpKSU+JQ0KICBnZ3Bsb3QoKSsNCiAgZ2VvbV9kZW5zaXR5X3JpZGdlc19ncmFkaWVudChhZXMoeD1PUix5PXJlb3JkZXIoUGFyYSxPUiksZmlsbD0uLnguLiksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2NhbGU9MSxhbHBoYT0wLjAxLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNob3cubGVnZW5kID1GKSsNCiAgc2NhbGVfZmlsbF92aXJpZGlzKG9wdGlvbj0iQyIsZGlyZWN0aW9uID0gLTEpKyAgDQogIGxhYnMoeD0iT2Rkcy1yYXRpbyIseT0iQmV0YXMiKSsNCiAgZ2VvbV92bGluZSh4aW50ZXJjZXB0ID0gMSxjb2w9ImJsdWUiLGxpbmV0eXBlPTIpKw0KICB0aGVtZV9idygpDQpgYGANCg0KIyMgTcO0IGjDrG5oIHPhu60gZOG7pW5nIEJlcm5vdWxsaShwKQ0KDQpgYGB7cn0NCiMgY3JlYXRlIG1hdHJpY2VzIHRvIGdyZXRhIGFycmF5cw0KZmVhdHVyZXMgPC0gYXNfZGF0YShkZlssLTEwXSkNCg0KWSA8LSBhc19kYXRhKGRmJENsYXNzKQ0KDQojIGNyZWF0ZSBncmV0YSBhcnJheXMgZm9yIHJhbmRvbSB2YXJpYWJsZXMNCmludGVyY2VwdCA8LSBub3JtYWwoMCwgMTApDQpiZXRhIDwtIG5vcm1hbCgwLCAxMCwgZGltID0gOUwpDQoNCiMgbG9naXQtbGluZWFyIG1vZGVsDQpsaW5lYXJfcHJlZGljdG9yIDwtIGludGVyY2VwdCArIGZlYXR1cmVzICUqJSBiZXRhDQoNCnByb2IgPC0gaWxvZ2l0KGxpbmVhcl9wcmVkaWN0b3IpDQoNCiMgZGlzdHJpYnV0aW9uIChsaWtlbGlob29kKSBvdmVyIG9ic2VydmVkIHZhbHVlcw0KZGlzdHJpYnV0aW9uKFkpIDwtIGJlcm5vdWxsaShwcm9iKQ0KDQptb2RfYmVybm91bGxpIDwtIGdyZXRhOjptb2RlbChpbnRlcmNlcHQsYmV0YSkNCg0KcGxvdChtb2RfYmVybm91bGxpKQ0KYGBgDQoNCiFbXShiZXJub3VsbGkucG5nKQ0KDQpUYSBsw6BtIHTGsMahbmcgdOG7sSBuaMawIHRyw6puIMSR4buDIGPDsyAxMCBjaHXhu5dpIE1DTUMgdsOgIHh14bqldCBr4bq/dCBxdeG6ow0KDQpgYGB7cn0NCmRyYXdzQiA8LSBtY21jKG1vZF9iZXJub3VsbGksDQogICAgICAgICAgICAgIHdhcm11cCA9IDIwMDAsDQogICAgICAgICAgICAgIG5fc2FtcGxlcyA9IDUwMDAsICAgICAgICAgICAgIA0KICAgICAgICAgICAgICBwYl91cGRhdGUgPSAxMDApDQoNCnBvc3RfZGYyPC1kcmF3c0JbWzFdXSU+JWFzLmRhdGEuZnJhbWUoKSU+JW11dGF0ZShpdGVyYXRpb249YygxOjUwMDApKQ0KDQpjb2xuYW1lcyhwb3N0X2RmMik9YygiSW50ZXJjZXB0Iixjb2xuYW1lcyhkZlssLTEwXSksIkl0ZXJhdGlvbiIpDQoNCg0KcG9zdF9kZjIlPiVnYXRoZXIoY29sbmFtZXMoZGZbLC0xMF0pLGtleT0iUGFyYSIsdmFsdWU9IlZhbHVlIiklPiUNCiAgbXV0YXRlKE9SPWV4cChWYWx1ZSkpJT4lDQogIGdncGxvdChhZXMoeD1PUikpKw0KICBnZW9tX2RlbnNpdHkoYWxwaGE9MC41LGFlcyhmaWxsPVBhcmEsY29sPVBhcmEpLHNob3cubGVnZW5kID0gRikrDQogIHRoZW1lX2J3KCkrDQogIGdlb21fdmxpbmUoeGludGVyY2VwdD0xLGNvbD0iYmx1ZTQiLGxpbmV0eXBlPTIpKw0KICBmYWNldF93cmFwKH5QYXJhLG5jb2w9MyxzY2FsZXM9ImZyZWUiKQ0KDQpwb3N0X2RmMiU+JWdhdGhlcihjb2xuYW1lcyhkZlssLTEwXSksa2V5PSJQYXJhIix2YWx1ZT0iVmFsdWUiKSU+JQ0KICBtdXRhdGUoT1I9ZXhwKFZhbHVlKSklPiUNCiAgZ2dwbG90KCkrDQogIGdlb21fZGVuc2l0eV9yaWRnZXNfZ3JhZGllbnQoYWVzKHg9T1IseT1yZW9yZGVyKFBhcmEsT1IpLGZpbGw9Li54Li4pLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNjYWxlPTEsYWxwaGE9MC4wMSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzaG93LmxlZ2VuZCA9RikrDQogIHNjYWxlX2ZpbGxfdmlyaWRpcyhvcHRpb249IkQiLGRpcmVjdGlvbiA9IC0xKSsgIA0KICBsYWJzKHg9Ik9kZHMtcmF0aW8iLHk9IkJldGFzIikrDQogIGdlb21fdmxpbmUoeGludGVyY2VwdCA9IDEsY29sPSJibHVlIixsaW5ldHlwZT0yKSsNCiAgdGhlbWVfYncoKQ0KYGBgDQoNCg0KIyBU4buVbmcga+G6v3QNCg0KQ2jDum5nIHRhIHbhu6thIHRo4buxYyBoaeG7h24gMiBiaeG6v24gdGjhu4MgY+G7p2EgbcO0IGjDrG5oIGjhu5NpIHF1eSBsb2dpc3RpYywgxJHDsyBsw6AgbeG6oW5nIG5ldXJvbiAxIGzhu5twIHbDoCBo4buTaSBxdXkgQmF5ZXMuIFR1eSBraMOhYyBuaGF1IHbhu4EgY8ahIGNo4bq/LCBj4bqjIDIgxJHhu4F1IGThu7FhIHbDoG8gVGVuc29yZmxvdy4gSHkgduG7jW5nIGLDoGkgdmnhur90IGdpw7pwIGPDoWMgYuG6oW4gbXVhIHZ1aSDEkcaw4bujYyB2w6BpIHBow7p0LiANCg0KQ3Xhu5FpIHR14bqnbiB2dWkgduG6uyAuLi4NCg==