1 Đặt vấn đề

Như đã trình bày trong trước, Nhi đang đặc biệt quan tâm đến việc vận dụng những phương pháp phân tích dữ liệu đặc biệt từ phái Machine learning vào nghiên cứu y học truyền thống, với hy vọng thực hiện được hai mục tiêu: 1) Giúp bác sĩ và sinh viên Y khoa thoát khỏi cái hộp của những quy trình cổ điển/lạc hậu, tăng khả năng giải quyết vấn đề và sáng tạo; và 2) Xóa bỏ dần khoảng cách và những dị biệt giữa 2 lĩnh vực: Khoa học máy tính (Machine learning) và Thống kê y học.

Trong bài này, Nhi muốn giới thiệu đến các bạn đồng nghiệp về một giải thuật (algorithm) cổ điển bên phái Machine learning, có tên là Naive Bayes, những ưu thế và hạn chế của nó khi áp dụng giải quyết vấn đề trong chuyên khoa Y.

2 Giới thiệu về Naive Bayes

Naive Bayes là một trong những giải thuật Machine learning cổ điển được đề cập nhiều trong thập niên 1950-1960 để giải quyết các bài toán phân loại văn bản. Phương pháp Naive Bayes có mối liên hệ mật thiết với ngành Thống kê vì cơ chế của nó dựa vào định lý Bayes và khi vận dụng vào Y học thì Naive Bayes cũng tương đồng với quy trình biện luận lâm sàng của người bác sĩ. Tuy cổ xưa và quá đơn giản, nhưng Naive Bayes vẫn còn chỗ đứng ở thời đại ngày nay; nó vẫn được nhắc đến trong mọi giáo trình về Machine learning bên cạnh những giải thuật phức tạp khác, điều này cho thấy Naive Bayes có hiệu quả thực sự, và đáng để ta tìm hiểu.

Naive Bayes là một giải thuật dựa vào lý thuyết xác suất điều kiện.Giả sử ta có một bài toán phân loại với kết quả là C gồm k nhãn giá trị. Mục tiêu của chúng ta là xếp một cá thể đặc trưng bởi vector dữ liệu X vào một phân lớp C gồm k loại. Điều chúng ta sẽ thực sự làm, đó là ước tính xác suất cho mỗi nhãn giá trị Ci, từ C1 đến Ck trong điều kiện X hiện có. Nhãn giá trị nào có xác suất cao nhất sẽ được chọn làm quyết định sau cùng. Theo định lý Bayes, ta có :

\[P(C_{i}|X) = \frac{P(C_{i})P(X|C_{i})}{P(X)}\]

Trong công thức này, P(Ci) được gọi là xác suất tiền nghiệm mà ta biết về Ci, trước khi tiếp cận dữ liệu X. Một thí dụ thường gặp về P(Ci) trong y học là tỉ suất mắc bệnh trong quần thể. Trong bài toán phân loại, P(Ci) được cung cấp từ chính tập dữ liệu ta dùng để huấn luyện mô hình, vì ta biết tỉ lệ phân bố của mỗi nhãn Ci trên toàn bộ mẫu.

Ở mẫu số, P(X) được hiểu như xác suất quan sát được (những) giá trị của vector dữ liệu X, trong toàn bộ khả năng có thể của chúng trên thực tế. Việc tính giá trị chính xác của P(X) gần như bất khả thi, nhưng may mắn thay, điều này không thực sự cần thiết, vì P(X) là mẫu số chung cho tất cả nhãn Ci. Do đó, hướng đi của chúng ta thay đổi một chút, đó là tối ưu hóa cho riêng tử số để đạt giá trị cực đại :

\[\arg max \frac{P(C_{i})P(X|C_{i})}{P(X)} = \arg max P(C_{i})P(X|C_{i})\]

Thành phần còn lại : p(X|Ci) là xác suất quan sát được giá trị dữ liệu X khi biết nhãn phân loại Ci. Việc tính xác suất điều kiện này cũng gần như bất khả thi trên thực tế, vì X là một không gian dữ liệu đa chiều gồm giá trị các biến ngẫu nhiên và tất cả những khả năng tổ hợp có thể trong số này một khi có liên hệ, tương tác giữa chúng.

Để giải quyết khó khăn này, người ta áp dụng một giả định quan trọng, đó là các yếu tố (biến số) xj trong dữ liệu X hoàn toàn độc lập với nhau. Lý thuyết xác suất cho ta biết rằng khi A và B là hai sự kiện độc lập với nhau, ta có xác suất cho trường hợp A và B đồng thời xảy ra bằng tích của xác suất riêng của A và B :

\[P(A\cap B) = p(A)p(B)\]

Như vậy với giả định các biến xj trong dữ liệu X độc lập với nhau, ta có thể ước tính P(X|Ci) như sau:

\[P(X|Ci)=\prod_{j=1}^{n}p(xj|Ci)\]

Giả định này rất phi lý và chắc chắn không thể tồn tại trong thế giới thực, do đó phương pháp này mới có tên gọi là “Naive Bayes”, tạm dịch “suy luận Bayes ngây thơ”.

Như vậy, quy trình phân loại vector dữ liệu đầu vào X chính là sự tối ưu hóa giá trị cực đại của tích các xác suất điều kiện cho từng biến riêng lẻ. Cuối cùng, ta chuyển từ tích sang tổng bằng hàm logarit :

\[\arg max P(C_{i}) \prod_{j=1}^{n} p(x_{j}|C_{i}) \propto \arg max \left ( log(p(C_{i}) +\sum_{j=1}^{n} log(p(x_{j}|C_{i}) \right )\]

Bây giờ chỉ còn việc tính xác suất điều kiện đơn biến độc lập p(xj|Ci). Tùy vào bản chất của yếu tố/đại lượng được khảo sát, ta có thể áp dụng quy luật phân bố Gaussian cho biến số liên tục, Bernoulli cho biến nhị phân hay ước tính tần suất đơn giản (biến nhị phân hoặc rời rạc nhiều giá trị).

Nếu xj là một biến định danh (rời rạc), thí dụ Giới tính Nam/nữ, trạng thái Có/không của triệu chứng, khi đó xác suất cần tìm chính là tỉ lệ trường hợp có giá trị Cj trên tổng số trường hợp.

\[p(x_{j}|C)=\frac{N_{Cj}}{N_{C}}\]

Trong trường hợp phân bố của biến rời rạc xj bị mất cân bằng (một level của x không hiện diện trong một phân lớp Ci), ta có thể hiệu chỉnh bằng Laplace smoothing:

\[\frac{N_{Cj} +\alpha}{N_{C} + d\alpha}\]

Với alpha = 1 cho tử số và da cho mẫu số để đảm bảo tổng xác suất = 1.

Một số package còn áp dụng kỹ thuật Kernel cho biến liên tục và kết quả thường tốt hơn so với giả định máy móc về phân bố Gaussian.

Nếu xj là biến liên tục, hoặc ta dùng Gaussian NB, hoặc ta có thể hoán chuyển nó thành một biến rời rạc bằng cách cắt tại nhiều ngưỡng, thí dụ tứ phân vị, hoặc nhiều khoảng nhỏ hơn nữa.

\[p(x_{j}|C) = p(x_{j}|\mu_{cj},\sigma_{Cj}^{2})=\frac{1}{\sqrt{2\pi}\sigma_{Cj}^{2}}exp\left ( -\frac{(x_{j}-\mu_{Cj})^{2}}{2\sigma_{Cj}^{2}} \right)\]

3 Thí dụ minh họa:

Trong bài này, Nhi sử dụng bộ số liệu về bệnh Suy giáp của tác giả J Ross Quinlan và viện Garvan (Úc) (1987). Dữ liệu này gồm hơn 3700 trường hợp với mục tiêu nghiên cứu (giả định) là xây dựng một mô hình chẩn đoán bệnh Suy Giáp (Hypothyroid) dựa vào thông tin gồm 6 biến số liên tục : Tuổi và giá trị các biomarker như hormone T3, TT4, T4U và FTI, và 12 biến nhị phân gồm Giới tính, điều trị thyroxine, có thai, phẫu thuật tuyến giáp, suy tuyến não thùy (hypopituitary ), triệu chứng bứu (goitre,tumor),tâm lý (psych), điều trị I131, lithium. Phân loại bệnh nhược giáp trong dữ liệu nguyên thủy có đến 5 nhãn kết quả là Negative, hypothyroid, primary hypothyroid, compensated hypothyroid và secondary hypothyroid. Để đơn giản hóa, ta sử dụng phiên bản giản lược của Quan Sun trên thư viện OpenML trong đó biến kết quả được giản lược chỉ còn 2 nhãn (nhị phân) là Positive và Negative.

Lưu ý: Trong dữ liệu gốc có 1 trường hợp mà biến Age bị nhập liệu sai, với giá trị 445 tuổi,ta loại trường hợp này khỏi dữ liệu phân tích.

library(tidyverse)

df=read.csv("https://www.openml.org/data/get_csv/53534/hypothyroid.arff",na.strings = "?")


df=df%>%dplyr::select(age,sex,pregnant,
               on.thyroxine,query.on.thyroxine,
               on.antithyroid.medication,
               thyroid.surgery,
               I131.treatment,sick,
               lithium,goitre,tumor,hypopituitary,psych,
               TSH,T3,TT4,T4U,FTI,binaryClass)

df=df%>%filter(.,age<100)

str(df)
## 'data.frame':    3770 obs. of  20 variables:
##  $ age                      : int  41 23 46 70 70 18 59 80 66 68 ...
##  $ sex                      : Factor w/ 2 levels "F","M": 1 1 2 1 1 1 1 1 1 2 ...
##  $ pregnant                 : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ on.thyroxine             : Factor w/ 2 levels "f","t": 1 1 1 2 1 2 1 1 1 1 ...
##  $ query.on.thyroxine       : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ on.antithyroid.medication: Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ thyroid.surgery          : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ I131.treatment           : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ sick                     : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ lithium                  : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ goitre                   : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ tumor                    : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 2 1 ...
##  $ hypopituitary            : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ psych                    : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ TSH                      : num  1.3 4.1 0.98 0.16 0.72 0.03 NA 2.2 0.6 2.4 ...
##  $ T3                       : num  2.5 2 NA 1.9 1.2 NA NA 0.6 2.2 1.6 ...
##  $ TT4                      : num  125 102 109 175 61 183 72 80 123 83 ...
##  $ T4U                      : num  1.14 NA 0.91 NA 0.87 1.3 0.92 0.7 0.93 0.89 ...
##  $ FTI                      : num  109 NA 120 NA 70 141 78 115 132 93 ...
##  $ binaryClass              : Factor w/ 2 levels "N","P": 2 2 2 2 2 2 2 2 2 2 ...

Đầu tiên, ta phân chia ngẫu nhiên dữ liệu gốc thành 2 phần, một dùng cho việc huấn luyện mô hình, một dùng để kiểm định mô hình này. Chúng ta không đụng đến testset cho đến khi kiểm định.

library(caret)

set.seed(1234)
idTrain=caret::createDataPartition(y=df$binaryClass, p=0.5,list=FALSE)

trainset=df[idTrain,]
testset=df[-idTrain,]

Tiếp theo ta sẽ thăm dò đặc tính phân bố của dữ liệu bằng biểu đồ. Đây là công đoạn đầu tiên trong mỗi nghiên cứu, nhưng đối với Naïve Bayes thì hình ảnh trực quan còn có ý nghĩa đặc biệt, vì những gì bạn nhìn thấy và cảm nhận lúc này cũng chính là những gì mà bạn sẽ thu được trong nội dung mô hình.

Do NB không xét liên hệ và tương tác giữa các biến, chúng ta chỉ quan tâm đến 2 điều: tính tương phản giữa các phân lớp và đặc tính phân bố của biến liên tục.

trainset[,-c(2:14)]%>%gather(age:FTI,key="Features",value="Score")%>%
  ggplot()+
  geom_density(aes(x=Score,fill=binaryClass),alpha=0.5)+
  facet_wrap(~Features,ncol=2,scales = "free")+
  theme_bw()+scale_fill_manual(values=c("blue","red"))

trainset[,-c(2:14)]%>%gather(age:FTI,key="Features",value="Score")%>%
  ggplot()+
  geom_violin(aes(x=binaryClass,y=Score,fill=binaryClass),alpha=0.5)+
  coord_flip()+
  facet_wrap(~Features,ncol=2,scales = "free")+
  theme_bw()+scale_fill_manual(values=c("blue","red"))

trainset[,c(2:14,20)]%>%gather(sex:psych,key="Features",value="Level")%>%na.omit()%>%
  ggplot()+
  geom_bar(aes(x=Level,y=..count..,fill=binaryClass),alpha=0.7)+
  coord_flip()+
  facet_wrap(~Features,ncol=3,scales = "free")+
  theme_bw(8)+scale_fill_manual(values=c("blue","red"))

plotfuncLow <- function(data,mapping){
  p <- ggplot(data = data,mapping=mapping)+
    stat_density2d(geom="polygon",aes(fill=trainset$binaryClass,alpha = ..level..))+
    geom_point(aes(fill=trainset$binaryClass,col=trainset$binaryClass),shape=".")+
    scale_fill_manual(values=c("blue","red"))+
    scale_color_manual(values=c("blue4","red4"))+
    theme_bw()
  p
}

plotdensfunc <- function(data,mapping){
  p <- ggplot(data = data,mapping=mapping)+
    stat_density(aes(fill=trainset$binaryClass,alpha = 0.7))+
    geom_rug(aes(col=trainset$binaryClass))+
    scale_fill_manual(values=c("blue","red"))+
    scale_color_manual(values=c("blue4","red4"))+
    theme_bw()
  p
}

library(GGally)

ggpairs(trainset[,-c(2:14,20)],lower = list(continuous=plotfuncLow),
        diag=list(continuous=plotdensfunc))

Dựa vào các biểu đồ, ta có thể phán đoán là: sự tương phản lớn nhất giữa 2 phân lớp P/N được biểu hiện tại các biến T4U, TT4,FTI, gợi ý đây là những biến số hữu hiệu trong mô hình.Ngoại trừ biến Age và TSH, các trị số hormone còn lại có hình dạng phân bố tương đối bình thường. Các biến nhị phân có giá trị False hầu hết đều có liên hệ với phân loại Positive của bệnh Nhược giáp. Do có sự mất cân bằng lớn trong các biến định tính, điển hình là biến hypopituitary thậm chí không có trường hợp nào thuộc loại T, do đó ta có thể phải dùng đến Laplace smoothing.

4 Package e1071

Mô hình Naive Bayes trong R có thể được dựng bằng 2 packages, đầu tiên là e1071.

hàm naiveBayes của package e1071 dựng mô hình Naive Bayes với tốc độ rất nhanh.Trong tùy chỉnh, ta khai báo na.omit để xử lý trường hợp thiếu sót dữ liệu riêng cho từng biến, và laplace=1.

Nội dung của mô hình cho thấy xác suất tiền nghiệm cho nhãn giá trị P là 0.915 và N là 0.085. Sau đó là một loạt các bảng chéo trình bày xác suất điều kiện từng giá trị của biến đầu vào trong điều kiện mỗi nhãn giá trị của biến đích nếu đầu vào là một biến rời rạc hay nhị phân (thí dụ Sex), hoặc Trung bình cho biến đầu vào liên tục.

Mô hình NBmod1 này rất nhạy, nhưng chưa đặc hiệu, nó vẫn còn chẩn đoán sai trong một số trường hợp Negative.

library(e1071)

NBmod1=e1071::naiveBayes(formula=binaryClass~.,
                         data=trainset,
                         na.action = na.omit,laplace=1)

NBmod1
## 
## Naive Bayes Classifier for Discrete Predictors
## 
## Call:
## naiveBayes.default(x = X, y = Y, laplace = laplace)
## 
## A-priori probabilities:
## Y
##          N          P 
## 0.08530084 0.91469916 
## 
## Conditional probabilities:
##    age
## Y       [,1]     [,2]
##   N 52.66071 17.07564
##   P 53.60783 19.40314
## 
##    sex
## Y           F         M
##   N 0.7982456 0.2017544
##   P 0.6608479 0.3391521
## 
##    pregnant
## Y            f          t
##   N 0.99122807 0.00877193
##   P 0.98254364 0.01745636
## 
##    on.thyroxine
## Y            f          t
##   N 0.97368421 0.02631579
##   P 0.89359933 0.10640067
## 
##    query.on.thyroxine
## Y             f           t
##   N 0.991228070 0.008771930
##   P 0.991687448 0.008312552
## 
##    on.antithyroid.medication
## Y            f          t
##   N 0.99122807 0.00877193
##   P 0.98171239 0.01828761
## 
##    thyroid.surgery
## Y            f          t
##   N 0.99122807 0.00877193
##   P 0.98254364 0.01745636
## 
##    I131.treatment
## Y            f          t
##   N 0.98245614 0.01754386
##   P 0.98420615 0.01579385
## 
##    sick
## Y            f          t
##   N 0.92105263 0.07894737
##   P 0.95261845 0.04738155
## 
##    lithium
## Y             f           t
##   N 0.991228070 0.008771930
##   P 0.995012469 0.004987531
## 
##    goitre
## Y             f           t
##   N 0.991228070 0.008771930
##   P 0.992518703 0.007481297
## 
##    tumor
## Y            f          t
##   N 0.96491228 0.03508772
##   P 0.97838736 0.02161264
## 
##    hypopituitary
## Y              f            t
##   N 0.9912280702 0.0087719298
##   P 0.9991687448 0.0008312552
## 
##    psych
## Y            f          t
##   N 0.97368421 0.02631579
##   P 0.93682461 0.06317539
## 
##    TSH
## Y        [,1]      [,2]
##   N 37.877679 70.383568
##   P  1.888293  6.009277
## 
##    T3
## Y       [,1]      [,2]
##   N 1.425893 0.7839316
##   P 2.025221 0.7287387
## 
##    TT4
## Y        [,1]     [,2]
##   N  70.93482 35.34950
##   P 111.21232 32.15326
## 
##    T4U
## Y        [,1]      [,2]
##   N 1.0155357 0.1973831
##   P 0.9920017 0.1890732
## 
##    FTI
## Y        [,1]     [,2]
##   N  71.26875 34.58914
##   P 113.08243 28.93413
predNB1=predict(NBmod1,testset)

caret::confusionMatrix(reference=testset$binaryClass,
                       data=predNB1,
                       positive="P",
                       mode="everything")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    N    P
##          N   60   13
##          P   85 1726
##                                          
##                Accuracy : 0.948          
##                  95% CI : (0.937, 0.9576)
##     No Information Rate : 0.923          
##     P-Value [Acc > NIR] : 1.136e-05      
##                                          
##                   Kappa : 0.526          
##  Mcnemar's Test P-Value : 7.387e-13      
##                                          
##             Sensitivity : 0.9925         
##             Specificity : 0.4138         
##          Pos Pred Value : 0.9531         
##          Neg Pred Value : 0.8219         
##               Precision : 0.9531         
##                  Recall : 0.9925         
##                      F1 : 0.9724         
##              Prevalence : 0.9230         
##          Detection Rate : 0.9161         
##    Detection Prevalence : 0.9613         
##       Balanced Accuracy : 0.7032         
##                                          
##        'Positive' Class : P              
## 

5 Package klaR

Một package khác cho phép dựng mô hình Naive Bayes đó là klaR.

Mô hình NBmod2 của klaR có nội dung và hiệu năng tương tự như mô hình NBmod1 của e1071.

library(klaR)

NBmod2=NaiveBayes(formula=binaryClass~.,
                  data=trainset,na.action=na.omit,fL=1,
                  usekernel=F)

NBmod2$tables
## $age
##       [,1]     [,2]
## N 52.66071 17.07564
## P 53.60783 19.40314
## 
## $sex
##         var
## grouping         F         M
##        N 0.7982456 0.2017544
##        P 0.6608479 0.3391521
## 
## $pregnant
##         var
## grouping         f          t
##        N 0.9912281 0.00877193
##        P 0.9825436 0.01745636
## 
## $on.thyroxine
##         var
## grouping         f          t
##        N 0.9736842 0.02631579
##        P 0.8935993 0.10640067
## 
## $query.on.thyroxine
##         var
## grouping         f           t
##        N 0.9912281 0.008771930
##        P 0.9916874 0.008312552
## 
## $on.antithyroid.medication
##         var
## grouping         f          t
##        N 0.9912281 0.00877193
##        P 0.9817124 0.01828761
## 
## $thyroid.surgery
##         var
## grouping         f          t
##        N 0.9912281 0.00877193
##        P 0.9825436 0.01745636
## 
## $I131.treatment
##         var
## grouping         f          t
##        N 0.9824561 0.01754386
##        P 0.9842062 0.01579385
## 
## $sick
##         var
## grouping         f          t
##        N 0.9210526 0.07894737
##        P 0.9526185 0.04738155
## 
## $lithium
##         var
## grouping         f           t
##        N 0.9912281 0.008771930
##        P 0.9950125 0.004987531
## 
## $goitre
##         var
## grouping         f           t
##        N 0.9912281 0.008771930
##        P 0.9925187 0.007481297
## 
## $tumor
##         var
## grouping         f          t
##        N 0.9649123 0.03508772
##        P 0.9783874 0.02161264
## 
## $hypopituitary
##         var
## grouping         f            t
##        N 0.9912281 0.0087719298
##        P 0.9991687 0.0008312552
## 
## $psych
##         var
## grouping         f          t
##        N 0.9736842 0.02631579
##        P 0.9368246 0.06317539
## 
## $TSH
##        [,1]      [,2]
## N 37.877679 70.383568
## P  1.888293  6.009277
## 
## $T3
##       [,1]      [,2]
## N 1.425893 0.7839316
## P 2.025221 0.7287387
## 
## $TT4
##        [,1]     [,2]
## N  70.93482 35.34950
## P 111.21232 32.15326
## 
## $T4U
##        [,1]      [,2]
## N 1.0155357 0.1973831
## P 0.9920017 0.1890732
## 
## $FTI
##        [,1]     [,2]
## N  71.26875 34.58914
## P 113.08243 28.93413
predNB2=predict(NBmod2,testset)

caret::confusionMatrix(reference=testset$binaryClass,
                       data=predNB2$class,
                       positive="P",
                       mode="everything")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    N    P
##          N   60   13
##          P   85 1726
##                                          
##                Accuracy : 0.948          
##                  95% CI : (0.937, 0.9576)
##     No Information Rate : 0.923          
##     P-Value [Acc > NIR] : 1.136e-05      
##                                          
##                   Kappa : 0.526          
##  Mcnemar's Test P-Value : 7.387e-13      
##                                          
##             Sensitivity : 0.9925         
##             Specificity : 0.4138         
##          Pos Pred Value : 0.9531         
##          Neg Pred Value : 0.8219         
##               Precision : 0.9531         
##                  Recall : 0.9925         
##                      F1 : 0.9724         
##              Prevalence : 0.9230         
##          Detection Rate : 0.9161         
##    Detection Prevalence : 0.9613         
##       Balanced Accuracy : 0.7032         
##                                          
##        'Positive' Class : P              
## 

Lưu ý là cả 2 mô hình này đều dùng giả định phân bố Gaussian cho tất cả biến liên tục. Ta có thể vẽ biểu đồ để quan sát hiện tượng các biến liên tục đã bị áp đặt phân bố Gaussian như thế nào: đây là hình ảnh hoàn toàn khác so với thực tế :

par(mfrow=c(2,3))

plot(0:0.1, xlim=c(0,98),ylim=c(0,0.03),
     ylab="density",type="n", 
     xlab="age")
curve(dnorm(x, NBmod2$tables$age[1,1], NBmod2$tables$age[1,2]), add=TRUE, col="blue")
curve(dnorm(x, NBmod2$tables$age[2,1], NBmod2$tables$age[2,2]), add=TRUE, col="red")

plot(0:300, xlim=c(0,300),ylim=c(0,0.09),
     ylab="density",type="n", 
     xlab="TSH")
curve(dnorm(x, NBmod2$tables$TSH[1,1], NBmod2$tables$TSH[1,2]), add=TRUE, col="blue")
curve(dnorm(x, NBmod2$tables$TSH[2,1], NBmod2$tables$TSH[2,2]), add=TRUE, col="red")

plot(0:6, xlim=c(0,6),ylim=c(0:1.5),
     ylab="density",type="n", 
     xlab="T3")
curve(dnorm(x, NBmod2$tables$T3[1,1], NBmod2$tables$T3[1,2]), add=TRUE, col="blue")
curve(dnorm(x, NBmod2$tables$T3[2,1], NBmod2$tables$T3[2,2]), add=TRUE, col="red")

plot(0:6, xlim=c(0,6),ylim=c(0,3),
     ylab="density",type="n", 
     xlab="T4U")
curve(dnorm(x, NBmod2$tables$T4U[1,1], NBmod2$tables$T4U[1,2]), add=TRUE, col="blue")
curve(dnorm(x, NBmod2$tables$T4U[2,1], NBmod2$tables$T4U[2,2]), add=TRUE, col="red")

plot(0:400,xlim=c(0,400),ylim=c(0,0.015),
     ylab="density",type="n", 
     xlab="TT4")
curve(dnorm(x, NBmod2$tables$TT4[1,1], NBmod2$tables$TT4[1,2]), add=TRUE, col="blue")
curve(dnorm(x, NBmod2$tables$TT4[2,1], NBmod2$tables$TT4[2,2]), add=TRUE, col="red")

plot(0:300,xlim=c(0,300),ylim=c(0,0.015),
     ylab="density",type="n", 
     xlab="FTI")
curve(dnorm(x, NBmod2$tables$FTI[1,1], NBmod2$tables$FTI[1,2]), add=TRUE, col="blue")
curve(dnorm(x, NBmod2$tables$FTI[2,1], NBmod2$tables$FTI[2,2]), add=TRUE, col="red")

Bây giờ chúng ta thực hiện một mô hình NB khác trong klaR, nhưng lần này có áp dụng phương pháp kernel để khảo sát chính xác hơn phân bố các biến liên tục.

NBmod2k=klaR::NaiveBayes(formula=binaryClass~.,
                        data=trainset,
                        na.action = na.omit,fL=1,
                        usekernel=T)

predNB2k=predict(NBmod2k,testset)

caret::confusionMatrix(reference=testset$binaryClass,
                       data=predNB2k$class,
                       positive="P",
                       mode="everything")
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction    N    P
##          N  107   43
##          P   38 1696
##                                           
##                Accuracy : 0.957           
##                  95% CI : (0.9468, 0.9657)
##     No Information Rate : 0.923           
##     P-Value [Acc > NIR] : 1.472e-09       
##                                           
##                   Kappa : 0.7021          
##  Mcnemar's Test P-Value : 0.6567          
##                                           
##             Sensitivity : 0.9753          
##             Specificity : 0.7379          
##          Pos Pred Value : 0.9781          
##          Neg Pred Value : 0.7133          
##               Precision : 0.9781          
##                  Recall : 0.9753          
##                      F1 : 0.9767          
##              Prevalence : 0.9230          
##          Detection Rate : 0.9002          
##    Detection Prevalence : 0.9204          
##       Balanced Accuracy : 0.8566          
##                                           
##        'Positive' Class : P               
## 

Mô hình NBmod2k có sử dụng kernel tốt hơn so với mô hình NBmod2 và 1; khi kiểm định trên testset, ta thấy mô hình NBmod2k có độ chính xác sau cân bằng cao hơn rõ rệt: BAC=0.86 so với 0.70, mức độ tương hợp thực tế và chẩn đoán rất cao với Kappa=0.70. Mô hình này rất nhạy và khá đặc hiệu.

Khi dùng phương pháp kernel, phân bố các biến liên tục có dạng như sau:

NBmod3=NaiveBayes(formula=binaryClass~.,
                 data=trainset[,-c(2:14)],
                 na.action = na.omit,fL=1,
                 usekernel=T)

par(mfrow=c(2,3))

plot(NBmod3,legendplot=T,col=c("blue","red"),lty=c(1,1))

Liên hệ giữa mỗi biến liên tục đầu vào và xác suất điều kiện cho nhãn P, N được khảo sát bằng biểu đồ sau:

testdf<-testset%>%mutate(Negative=predNB2k$posterior[,1],
                       Positive=predNB2k$posterior[,2],
                       Label=predNB2k$class)

testdf[,-c(2:14)]%>%gather(age:FTI,key="Features",value="Score")%>%na.omit()%>%
  gather(Negative,Positive,key="Type",value="Posterior")%>%
  ggplot()+
  geom_smooth(aes(x=Score,y=Posterior,
                  col=Type,fill=Type),
              method="loess",alpha=0.2)+
  facet_wrap(~Features,ncol=2,scales = "free")+
  theme_bw()+
  scale_color_manual(values=c("blue","red"))+
  scale_fill_manual(values=c("blue","red"))

Còn đây là hiệu năng của mô hình NBmod2k

testdf[,-c(2:14)]%>%gather(age:FTI,key="Features",value="Score")%>%na.omit()%>%
  mutate(.,Accuracy=if_else(binaryClass==Label,"Correct","Failed"))%>%
  ggplot()+
  geom_bar(aes(x=binaryClass,y=..count..,fill=Accuracy),position="fill",alpha=0.8)+
  theme_bw()+coord_flip()+
scale_fill_manual(values=c("blue","red"))

Ta làm thêm 2 biểu đồ khác để đánh giá về mức quan trọng của từng biến liên tục và nhị phân:

numdfy=data.frame(age=NBmod2k$tables$age$P$y,
                  TSH=NBmod2k$tables$TSH$P$y,
                  T3=NBmod2k$tables$T3$P$y,
                  TT4=NBmod2k$tables$TT4$P$y,
                  T4U=NBmod2k$tables$T4U$P$y,
                  FTI=NBmod2k$tables$FTI$P$y)%>%gather(age:FTI,key="Features",value="Density")

catdf=data.frame(PregF=NBmod2k$tables$pregnant[2,1],
                 ThyroxF=NBmod2k$tables$on.thyroxine[2,1],
                 QueryF=NBmod2k$tables$query.on.thyroxine[2,1],
                 MedF=NBmod2k$tables$on.antithyroid.medication[2,1],
                 SurgF=NBmod2k$tables$thyroid.surgery[2,1],
                 I131F=NBmod2k$tables$I131.treatment[2,1],
                 SickF=NBmod2k$tables$sick[2,1],
                 GoitreF=NBmod2k$tables$goitre[2,1],
                 LithiumF=NBmod2k$tables$lithium[2,1],
                 TumorF=NBmod2k$tables$tumor[2,1],
                 HypoPitF=NBmod2k$tables$hypopituitary[2,1],
                 PsychF=NBmod2k$tables$psych[2,1])
numdfy%>%
  ggplot(aes(x=reorder(Features,Density),y=Density,fill=reorder(Features,Density)))+
  geom_boxplot(show.legend = F)+
  scale_y_continuous(labels=NULL,"Contribution level")+
  scale_x_discrete("Features")+
  coord_flip()+theme_bw()

catdf%>%gather(PregF:PsychF,key="Feature",value="Posterior")%>%
  ggplot(aes(x=reorder(Feature,Posterior),y=Posterior,fill=reorder(Feature,Posterior)))+
  geom_text(aes(label=Feature),nudge_x = 0.1,nudge_y = 0.01)+
  geom_point(shape=21,col="black",size=5,show.legend = F)+
  theme_bw()+
  scale_y_continuous(labels=NULL,"Contribution level")+
  scale_x_discrete(labels=NULL,"Features")

6 Ưu thế và Giới hạn

Sau đây là những ưu thế và nhược điểm của giải thuật Naïve Bayes khi áp dụng cho nghiên cứu y học :

6.1 Ưu thế

Cơ chế hoạt động và kết quả mô hình dựng bằng Naive Bayes tương đồng với quy trình suy luận trong thực hành lâm sàng. Thật vậy, quy trình chẩn đoán mà bạn đang làm hằng ngày trên mỗi bệnh nhân là sự tổng hợp của hàng loạt suy luận Bayes theo kiểu : Với dữ liệu lâm sàng trong bệnh án (tiền sử, bệnh sử, triệu chứng chức năng/thực thể, kết quả xét nghiệm…) thì xác suất mắc bệnh của bệnh nhân là bao nhiêu ?

Về mặt kỹ thuật NB có nhiều ưu thế: nó khai phá dữ liệu với tốc độ cực kì nhanh, vì 2 lý do – thứ nhất NB xét riêng lẻ từng biến nhưng không cần biết mối liên hệ, tổ hợp giữa chúng, thứ hai vì tại mỗi biến, xác suất điều kiện riêng phần được ước lượng đơn giản bằng phép đếm tần suất hoặc giả định phân phối chuẩn. Tương tự, khi thi hành nhiệm vụ trên dữ liệu mới thì Naive Baye cũng cực kì nhanh. Quả thực, hiếm giải thuật nào có tốc độ nhanh trong cả 2 quá trình: học từ dữ liệu và thi hành nhiệm vụ như Naive Bayes. Do đó, NB rất thích hợp cho những bộ dữ liệu kích thước lớn, cả về số lượng biến và số trường hợp.

Một ưu thế hiển nhiên khác là tính phổ quát: Naive Bayes chấp nhận tất cả các loại biến trong dữ liệu đầu vào, từ liên tục, rời rạc cho đến nhị phân.

Mặt khác, NB chỉ cần cỡ mẫu vừa đủ cho mỗi biến, vì thực chất nó không dùng hết toàn bộ từng trường hợp mà chỉ quan tâm đến tỉ lệ phân bố cho mỗi bậc giá trị (biến rời rạc) hoặc đặc tính phân phối (biến liên tục). Cũng vì lý do này NB ít nhạy cảm với nhiễu và chấp nhận dữ liệu bị thiếu sót rải rác cho từng biến. Trong thực hành lâm sàng không phải lúc nào ta cũng thu thập được đầy đủ thông tin, một mô hình chấp nhận thiếu sót dữ liệu như Naive Bayes có thể sẽ có ích trong trường hợp này.

Việc dựng mô hình NB vô cùng đơn giản. Một mặt, do giả định về tính độc lập, những biến số vô hiệu, ảnh hưởng rất ít đến kết quả của mô hình, trong khi các biến số quan trọng vẫn thực hiện độc lập vai trò của mình. Điều này có nghĩa là khi dùng NB, bạn không cần quan tâm đến việc chọn lọc biến số và mất thời gian xét các giả thuyết về tương tác đa chiều, mà chỉ cần chuyển toàn bộ dữ liệu đầu vào cho NB tự lo. Bạn cũng có thể dùng NB vô tư cho những bộ dữ liệu có rất nhiều biến và chưa có bất cứ ý tưởng nào rõ ràng về vai trò từng biến cũng như liên hệ giữa chúng.

Nội dung mô hình Naive Bayes cũng rất đơn giản và dễ hiểu, nó gần giống như một phân tích thống kê đơn biến hàng loạt.

Nếu mục tiêu của nghiên cứu là phân loại chính xác, Naive Bayes có hiệu quả một cách đáng ngạc nhiên. Tuy đơn giản (và phi lý), nhưng hiệu năng của mô hình NB không thua kém bất cứ giải thuật nào khác, kể cả những phương pháp phức tạp hơn rất nhiều. Độ chính xác của Naive Bayes từng được chứng minh trên thực tế, thậm chí cho những bài toán phân loại tới 10-20 nhãn giá trị.

6.2 Hạn chế

Bất lợi lớn nhất của Naive Bayes chính là sự đơn giản quá mức của nó. Giả định về tính độc lập tuyệt đối giữa các biến đầu vào là rất vô lý và hoàn toàn mâu thuẫn với cơ chế sinh lý bệnh. Do đó mô hình Naive Bayes thường không cho phép diễn giải về tương tác đa chiều hoặc khai phá những cơ chế sinh lý bệnh học mới.

Một nhược điểm khác của Naive Bayes đó là nó nhạy cảm với vấn đề mất cân bằng giữa các nhãn phân loại trong dữ liệu. Tuy nhiên đây chỉ là vấn đề kỹ thuật và chúng ta có thể khắc phục nó.

7 Kết luận

Naive Bayes là một giải thuật Machine learning tuy đơn giản nhưng rất hiệu quả khi áp dụng cho bài toán phân loại. Cơ chế hoạt động của Naive Bayes gần như tương đồng với biện luận chẩn đoán trên lâm sàng, ngoại trừ giả định phi lý về tính độc lập giữa các triệu chứng và biomarker. Tuy nhiên so với những phương pháp thống kê cổ điển khác như kiểm định Ki bình phương, t test, kết quả của Naive Baye cung cấp nhiều thông tin hơn, như xác suất điều kiện cho từng nhãn giá trị, có sử dụng xác suất tiền định. Naive Bayes còn cho phép tổng hợp thông tin từ phân tích đơn biến thành quy luật chẩn đoán, và quy luật này rất hiệu quả như ta thấy.

LS0tDQp0aXRsZTogIk5haXZlIEJheWVzIOG7qW5nIGThu6VuZyBjaG8geSBo4buNYyIgDQphdXRob3I6ICJMw6ogTmfhu41jIEto4bqjIE5oaSINCmRhdGU6ICIxOSBUaMOhbmcgMSAyMDE4Ig0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgbnVtYmVyX3NlY3Rpb25zOiB5ZXMNCiAgICB0aGVtZTogImRlZmF1bHQiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSkNCmxpYnJhcnkodGlkeXZlcnNlKQ0KYGBgDQoNCiFbXShOYWl2ZUJheWVzZmlnMS5wbmcpDQoNCiMgxJDhurd0IHbhuqVuIMSR4buBDQoNCk5oxrAgxJHDoyB0csOsbmggYsOgeSB0cm9uZyB0csaw4bubYywgTmhpIMSRYW5nIMSR4bq3YyBiaeG7h3QgcXVhbiB0w6JtIMSR4bq/biB2aeG7h2MgduG6rW4gZOG7pW5nIG5o4buvbmcgcGjGsMahbmcgcGjDoXAgcGjDom4gdMOtY2ggZOG7ryBsaeG7h3UgxJHhurdjIGJp4buHdCB04burIHBow6FpIE1hY2hpbmUgbGVhcm5pbmcgdsOgbyBuZ2hpw6puIGPhu6l1IHkgaOG7jWMgdHJ1eeG7gW4gdGjhu5FuZywgduG7m2kgaHkgduG7jW5nIHRo4buxYyBoaeG7h24gxJHGsOG7o2MgaGFpIG3hu6VjIHRpw6p1OiAxKSBHacO6cCBiw6FjIHPEqSB2w6Agc2luaCB2acOqbiBZIGtob2EgdGhvw6F0IGto4buPaSBjw6FpIGjhu5lwIGPhu6dhIG5o4buvbmcgcXV5IHRyw6xuaCBj4buVIMSRaeG7g24vbOG6oWMgaOG6rXUsIHTEg25nIGto4bqjIG7Eg25nIGdp4bqjaSBxdXnhur90IHbhuqVuIMSR4buBIHbDoCBzw6FuZyB04bqhbzsgdsOgIDIpIFjDs2EgYuG7jyBk4bqnbiBraG/huqNuZyBjw6FjaCB2w6Agbmjhu69uZyBk4buLIGJp4buHdCBnaeG7r2EgMiBsxKluaCB24buxYzogS2hvYSBo4buNYyBtw6F5IHTDrW5oIChNYWNoaW5lIGxlYXJuaW5nKSB2w6AgVGjhu5FuZyBrw6ogeSBo4buNYy4NCg0KIVtdKE5CMC5wbmcpDQoNClRyb25nIGLDoGkgbsOgeSwgTmhpIG114buRbiBnaeG7m2kgdGhp4buHdSDEkeG6v24gY8OhYyBi4bqhbiDEkeG7k25nIG5naGnhu4dwIHbhu4EgbeG7mXQgZ2nhuqNpIHRodeG6rXQgKGFsZ29yaXRobSkgY+G7lSDEkWnhu4NuIGLDqm4gcGjDoWkgTWFjaGluZSBsZWFybmluZywgY8OzIHTDqm4gbMOgIE5haXZlIEJheWVzLCBuaOG7r25nIMawdSB0aOG6vyB2w6AgaOG6oW4gY2jhur8gY+G7p2EgbsOzIGtoaSDDoXAgZOG7pW5nIGdp4bqjaSBxdXnhur90IHbhuqVuIMSR4buBIHRyb25nIGNodXnDqm4ga2hvYSBZLg0KDQojIEdp4bubaSB0aGnhu4d1IHbhu4EgTmFpdmUgQmF5ZXMgDQoNCk5haXZlIEJheWVzIGzDoCBt4buZdCB0cm9uZyBuaOG7r25nIGdp4bqjaSB0aHXhuq10IE1hY2hpbmUgbGVhcm5pbmcgY+G7lSDEkWnhu4NuIMSRxrDhu6NjIMSR4buBIGPhuq1wIG5oaeG7gXUgdHJvbmcgdGjhuq1wIG5pw6puIDE5NTAtMTk2MCDEkeG7gyBnaeG6o2kgcXV54bq/dCBjw6FjIGLDoGkgdG/DoW4gcGjDom4gbG/huqFpIHbEg24gYuG6o24uIFBoxrDGoW5nIHBow6FwIE5haXZlIEJheWVzIGPDsyBt4buRaSBsacOqbiBo4buHIG3huq10IHRoaeG6v3QgduG7m2kgbmfDoG5oIFRo4buRbmcga8OqIHbDrCBjxqEgY2jhur8gY+G7p2EgbsOzIGThu7FhIHbDoG8gxJHhu4tuaCBsw70gQmF5ZXMgdsOgIGtoaSB24bqtbiBk4bulbmcgdsOgbyBZIGjhu41jIHRow6wgTmFpdmUgQmF5ZXMgY8WpbmcgdMawxqFuZyDEkeG7k25nIHbhu5tpIHF1eSB0csOsbmggYmnhu4duIGx14bqtbiBsw6JtIHPDoG5nIGPhu6dhIG5nxrDhu51pIGLDoWMgc8SpLiBUdXkgY+G7lSB4xrBhIHbDoCBxdcOhIMSRxqFuIGdp4bqjbiwgbmjGsG5nIE5haXZlIEJheWVzIHbhuqtuIGPDsm4gY2jhu5cgxJHhu6luZyDhu58gdGjhu51pIMSR4bqhaSBuZ8OgeSBuYXk7IG7DsyB24bqrbiDEkcaw4bujYyBuaOG6r2MgxJHhur9uIHRyb25nIG3hu41pIGdpw6FvIHRyw6xuaCB24buBIE1hY2hpbmUgbGVhcm5pbmcgYsOqbiBj4bqhbmggbmjhu69uZyBnaeG6o2kgdGh14bqtdCBwaOG7qWMgdOG6oXAga2jDoWMsIMSRaeG7gXUgbsOgeSBjaG8gdGjhuqV5IE5haXZlIEJheWVzIGPDsyBoaeG7h3UgcXXhuqMgdGjhu7FjIHPhu7EsIHbDoCDEkcOhbmcgxJHhu4MgdGEgdMOsbSBoaeG7g3UuDQoNCk5haXZlIEJheWVzIGzDoCBt4buZdCBnaeG6o2kgdGh14bqtdCBk4buxYSB2w6BvIGzDvSB0aHV54bq/dCB4w6FjIHN14bqldCDEkWnhu4F1IGtp4buHbi5HaeG6oyBz4butIHRhIGPDsyBt4buZdCBiw6BpIHRvw6FuIHBow6JuIGxv4bqhaSB24bubaSBr4bq/dCBxdeG6oyBsw6AgQyBn4buTbSBrIG5ow6NuIGdpw6EgdHLhu4suIE3hu6VjIHRpw6p1IGPhu6dhIGNow7puZyB0YSBsw6AgeOG6v3AgbeG7mXQgY8OhIHRo4buDIMSR4bq3YyB0csawbmcgYuG7n2kgdmVjdG9yIGThu68gbGnhu4d1IFggdsOgbyBt4buZdCBwaMOibiBs4bubcCBDIGfhu5NtIGsgbG/huqFpLiDEkGnhu4F1IGNow7puZyB0YSBz4bq9IHRo4buxYyBz4buxIGzDoG0sIMSRw7MgbMOgIMaw4bubYyB0w61uaCB4w6FjIHN14bqldCBjaG8gbeG7l2kgbmjDo24gZ2nDoSB0cuG7iyBDaSwgdOG7qyBDMSDEkeG6v24gQ2sgdHJvbmcgxJFp4buBdSBraeG7h24gWCBoaeG7h24gY8OzLiBOaMOjbiBnacOhIHRy4buLIG7DoG8gY8OzIHjDoWMgc3XhuqV0IGNhbyBuaOG6pXQgc+G6vSDEkcaw4bujYyBjaOG7jW4gbMOgbSBxdXnhur90IMSR4buLbmggc2F1IGPDuW5nLiBUaGVvIMSR4buLbmggbMO9IEJheWVzLCB0YSBjw7MgOg0KDQokJFAoQ197aX18WCkgPSBcZnJhY3tQKENfe2l9KVAoWHxDX3tpfSl9e1AoWCl9JCQNCg0KVHJvbmcgY8O0bmcgdGjhu6ljIG7DoHksIFAoQ2kpIMSRxrDhu6NjIGfhu41pIGzDoCB4w6FjIHN14bqldCB0aeG7gW4gbmdoaeG7h20gbcOgIHRhIGJp4bq/dCB24buBIENpLCB0csaw4bubYyBraGkgdGnhur9wIGPhuq1uIGThu68gbGnhu4d1IFguIE3hu5l0IHRow60gZOG7pSB0aMaw4budbmcgZ+G6t3AgduG7gSBQKENpKSB0cm9uZyB5IGjhu41jIGzDoCB04buJIHN14bqldCBt4bqvYyBi4buHbmggdHJvbmcgcXXhuqduIHRo4buDLiBUcm9uZyBiw6BpIHRvw6FuIHBow6JuIGxv4bqhaSwgUChDaSkgxJHGsOG7o2MgY3VuZyBj4bqlcCB04burIGNow61uaCB04bqtcCBk4buvIGxp4buHdSB0YSBkw7luZyDEkeG7gyBodeG6pW4gbHV54buHbiBtw7QgaMOsbmgsIHbDrCB0YSBiaeG6v3QgdOG7iSBs4buHIHBow6JuIGLhu5EgY+G7p2EgbeG7l2kgbmjDo24gQ2kgdHLDqm4gdG/DoG4gYuG7mSBt4bqrdS4gDQoNCuG7niBt4bqrdSBz4buRLCBQKFgpIMSRxrDhu6NjIGhp4buDdSBuaMawIHjDoWMgc3XhuqV0IHF1YW4gc8OhdCDEkcaw4bujYyAobmjhu69uZykgZ2nDoSB0cuG7iyBj4bunYSB2ZWN0b3IgZOG7ryBsaeG7h3UgWCwgdHJvbmcgdG/DoG4gYuG7mSBraOG6oyBuxINuZyBjw7MgdGjhu4MgY+G7p2EgY2jDum5nIHRyw6puIHRo4buxYyB04bq/LiBWaeG7h2MgdMOtbmggZ2nDoSB0cuG7iyBjaMOtbmggeMOhYyBj4bunYSBQKFgpIGfhuqduIG5oxrAgYuG6pXQga2jhuqMgdGhpLCBuaMawbmcgbWF5IG3huq9uIHRoYXksIMSRaeG7gXUgbsOgeSBraMO0bmcgdGjhu7FjIHPhu7EgY+G6p24gdGhp4bq/dCwgdsOsIFAoWCkgbMOgIG3huqt1IHPhu5EgY2h1bmcgY2hvIHThuqV0IGPhuqMgbmjDo24gQ2kuIERvIMSRw7MsIGjGsOG7m25nIMSRaSBj4bunYSBjaMO6bmcgdGEgdGhheSDEkeG7lWkgbeG7mXQgY2jDunQsIMSRw7MgbMOgIHThu5FpIMawdSBow7NhIGNobyByacOqbmcgdOG7rSBz4buRIMSR4buDIMSR4bqhdCBnacOhIHRy4buLIGPhu7FjIMSR4bqhaSA6DQoNCiQkXGFyZyBtYXggXGZyYWN7UChDX3tpfSlQKFh8Q197aX0pfXtQKFgpfSA9IFxhcmcgbWF4IFAoQ197aX0pUChYfENfe2l9KSQkDQoNClRow6BuaCBwaOG6p24gY8OybiBs4bqhaSA6IHAoWHxDaSkgbMOgIHjDoWMgc3XhuqV0IHF1YW4gc8OhdCDEkcaw4bujYyBnacOhIHRy4buLIGThu68gbGnhu4d1IFgga2hpIGJp4bq/dCBuaMOjbiBwaMOibiBsb+G6oWkgQ2kuIFZp4buHYyB0w61uaCB4w6FjIHN14bqldCDEkWnhu4F1IGtp4buHbiBuw6B5IGPFqW5nIGfhuqduIG5oxrAgYuG6pXQga2jhuqMgdGhpIHRyw6puIHRo4buxYyB04bq/LCB2w6wgWCBsw6AgbeG7mXQga2jDtG5nIGdpYW4gZOG7ryBsaeG7h3UgxJFhIGNoaeG7gXUgZ+G7k20gZ2nDoSB0cuG7iyBjw6FjIGJp4bq/biBuZ+G6q3Ugbmhpw6puIHbDoCB04bqldCBj4bqjIG5o4buvbmcga2jhuqMgbsSDbmcgdOG7lSBo4bujcCBjw7MgdGjhu4MgdHJvbmcgc+G7kSBuw6B5IG3hu5l0IGtoaSBjw7MgbGnDqm4gaOG7hywgdMawxqFuZyB0w6FjIGdp4buvYSBjaMO6bmcuIA0KDQrEkOG7gyBnaeG6o2kgcXV54bq/dCBraMOzIGtoxINuIG7DoHksIG5nxrDhu51pIHRhIMOhcCBk4bulbmcgbeG7mXQgZ2nhuqMgxJHhu4tuaCBxdWFuIHRy4buNbmcsIMSRw7MgbMOgIGPDoWMgeeG6v3UgdOG7kSAoYmnhur9uIHPhu5EpIHhqIHRyb25nIGThu68gbGnhu4d1IFggaG/DoG4gdG/DoG4gxJHhu5ljIGzhuq1wIHbhu5tpIG5oYXUuIEzDvSB0aHV54bq/dCB4w6FjIHN14bqldCBjaG8gdGEgYmnhur90IHLhurFuZyBraGkgQSB2w6AgQiBsw6AgaGFpIHPhu7Ega2nhu4duIMSR4buZYyBs4bqtcCB24bubaSBuaGF1LCB0YSBjw7MgeMOhYyBzdeG6pXQgY2hvIHRyxrDhu51uZyBo4bujcCBBIHbDoCBCIMSR4buTbmcgdGjhu51pIHjhuqN5IHJhIGLhurFuZyB0w61jaCBj4bunYSB4w6FjIHN14bqldCByacOqbmcgY+G7p2EgQSB2w6AgQiA6DQoNCiQkUChBXGNhcCBCKSA9IHAoQSlwKEIpJCQNCg0KTmjGsCB24bqteSB24bubaSBnaeG6oyDEkeG7i25oIGPDoWMgYmnhur9uIHhqIHRyb25nIGThu68gbGnhu4d1IFggxJHhu5ljIGzhuq1wIHbhu5tpIG5oYXUsIHRhIGPDsyB0aOG7gyDGsOG7m2MgdMOtbmggUChYfENpKSBuaMawIHNhdToNCg0KJCRQKFh8Q2kpPVxwcm9kX3tqPTF9XntufXAoeGp8Q2kpJCQNCg0KR2nhuqMgxJHhu4tuaCBuw6B5IHLhuqV0IHBoaSBsw70gdsOgIGNo4bqvYyBjaOG6r24ga2jDtG5nIHRo4buDIHThu5NuIHThuqFpIHRyb25nIHRo4bq/IGdp4bubaSB0aOG7sWMsIGRvIMSRw7MgcGjGsMahbmcgcGjDoXAgbsOgeSBt4bubaSBjw7MgdMOqbiBn4buNaSBsw6Ag4oCcTmFpdmUgQmF5ZXPigJ0sIHThuqFtIGThu4tjaCDigJxzdXkgbHXhuq1uIEJheWVzIG5nw6J5IHRoxqHigJ0uDQoNCk5oxrAgduG6rXksIHF1eSB0csOsbmggcGjDom4gbG/huqFpIHZlY3RvciBk4buvIGxp4buHdSDEkeG6p3UgdsOgbyBYIGNow61uaCBsw6Agc+G7sSB04buRaSDGsHUgaMOzYSBnacOhIHRy4buLIGPhu7FjIMSR4bqhaSBj4bunYSB0w61jaCBjw6FjIHjDoWMgc3XhuqV0IMSRaeG7gXUga2nhu4duIGNobyB04burbmcgYmnhur9uIHJpw6puZyBs4bq7LiBDdeG7kWkgY8O5bmcsIHRhIGNodXnhu4NuIHThu6sgdMOtY2ggc2FuZyB04buVbmcgYuG6sW5nIGjDoG0gbG9nYXJpdCA6DQoNCiQkXGFyZyBtYXggUChDX3tpfSkgXHByb2Rfe2o9MX1ee259IHAoeF97an18Q197aX0pIFxwcm9wdG8gIFxhcmcgbWF4IFxsZWZ0ICggbG9nKHAoQ197aX0pICtcc3VtX3tqPTF9XntufSBsb2cocCh4X3tqfXxDX3tpfSkgXHJpZ2h0ICkkJA0KDQpCw6J5IGdp4budIGNo4buJIGPDsm4gdmnhu4djIHTDrW5oIHjDoWMgc3XhuqV0IMSRaeG7gXUga2nhu4duIMSRxqFuIGJp4bq/biDEkeG7mWMgbOG6rXAgcCh4anxDaSkuIFTDuXkgdsOgbyBi4bqjbiBjaOG6pXQgY+G7p2EgeeG6v3UgdOG7kS/EkeG6oWkgbMaw4bujbmcgxJHGsOG7o2Mga2jhuqNvIHPDoXQsIHRhIGPDsyB0aOG7gyDDoXAgZOG7pW5nIHF1eSBsdeG6rXQgcGjDom4gYuG7kSBHYXVzc2lhbiBjaG8gYmnhur9uIHPhu5EgbGnDqm4gdOG7pWMsIEJlcm5vdWxsaSBjaG8gYmnhur9uIG5o4buLIHBow6JuIGhheSDGsOG7m2MgdMOtbmggdOG6p24gc3XhuqV0IMSRxqFuIGdp4bqjbiAoYmnhur9uIG5o4buLIHBow6JuIGhv4bq3YyBy4budaSBy4bqhYyBuaGnhu4F1IGdpw6EgdHLhu4spLiANCg0KTuG6v3UgeGogbMOgIG3hu5l0IGJp4bq/biDEkeG7i25oIGRhbmggKHLhu51pIHLhuqFjKSwgdGjDrSBk4bulIEdp4bubaSB0w61uaCBOYW0vbuG7rywgdHLhuqFuZyB0aMOhaSBDw7Mva2jDtG5nIGPhu6dhIHRyaeG7h3UgY2jhu6luZywga2hpIMSRw7MgeMOhYyBzdeG6pXQgY+G6p24gdMOsbSBjaMOtbmggbMOgIHThu4kgbOG7hyB0csaw4budbmcgaOG7o3AgY8OzIGdpw6EgdHLhu4sgQ2ogdHLDqm4gdOG7lW5nIHPhu5EgdHLGsOG7nW5nIGjhu6NwLg0KDQokJHAoeF97an18Qyk9XGZyYWN7Tl97Q2p9fXtOX3tDfX0kJA0KDQpUcm9uZyB0csaw4budbmcgaOG7o3AgcGjDom4gYuG7kSBj4bunYSBiaeG6v24gcuG7nWkgcuG6oWMgeGogYuG7iyBt4bqldCBjw6JuIGLhurFuZyAobeG7mXQgbGV2ZWwgY+G7p2EgeCBraMO0bmcgaGnhu4duIGRp4buHbiB0cm9uZyBt4buZdCBwaMOibiBs4bubcCBDaSksIHRhIGPDsyB0aOG7gyBoaeG7h3UgY2jhu4luaCBi4bqxbmcgTGFwbGFjZSBzbW9vdGhpbmc6DQoNCiQkXGZyYWN7Tl97Q2p9ICtcYWxwaGF9e05fe0N9ICsgZFxhbHBoYX0kJA0KDQpW4bubaSBhbHBoYSA9IDEgY2hvIHThu60gc+G7kSB2w6AgZGEgY2hvIG3huqt1IHPhu5EgxJHhu4MgxJHhuqNtIGLhuqNvIHThu5VuZyB4w6FjIHN14bqldCA9IDEuIA0KDQpN4buZdCBz4buRIHBhY2thZ2UgY8OybiDDoXAgZOG7pW5nIGvhu7kgdGh14bqtdCBLZXJuZWwgY2hvIGJp4bq/biBsacOqbiB04bulYyB2w6Aga+G6v3QgcXXhuqMgdGjGsOG7nW5nIHThu5F0IGjGoW4gc28gduG7m2kgZ2nhuqMgxJHhu4tuaCBtw6F5IG3Ds2MgduG7gSBwaMOibiBi4buRIEdhdXNzaWFuLg0KDQpO4bq/dSB4aiBsw6AgYmnhur9uIGxpw6puIHThu6VjLCBob+G6t2MgdGEgZMO5bmcgR2F1c3NpYW4gTkIsIGhv4bq3YyB0YSBjw7MgdGjhu4MgaG/DoW4gY2h1eeG7g24gbsOzIHRow6BuaCBt4buZdCBiaeG6v24gcuG7nWkgcuG6oWMgYuG6sW5nIGPDoWNoIGPhuq90IHThuqFpIG5oaeG7gXUgbmfGsOG7oW5nLCB0aMOtIGThu6UgdOG7qSBwaMOibiB24buLLCBob+G6t2Mgbmhp4buBdSBraG/huqNuZyBuaOG7jyBoxqFuIG7hu69hLg0KDQokJHAoeF97an18QykgPSBwKHhfe2p9fFxtdV97Y2p9LFxzaWdtYV97Q2p9XnsyfSk9XGZyYWN7MX17XHNxcnR7MlxwaX1cc2lnbWFfe0NqfV57Mn19ZXhwXGxlZnQgKCAtXGZyYWN7KHhfe2p9LVxtdV97Q2p9KV57Mn19ezJcc2lnbWFfe0NqfV57Mn19IFxyaWdodCkkJA0KDQojIFRow60gZOG7pSBtaW5oIGjhu41hOiANCg0KVHJvbmcgYsOgaSBuw6B5LCBOaGkgc+G7rSBk4bulbmcgYuG7mSBz4buRIGxp4buHdSB24buBIGLhu4duaCBTdXkgZ2nDoXAgY+G7p2EgdMOhYyBnaeG6oyBKIFJvc3MgUXVpbmxhbiB2w6Agdmnhu4duIEdhcnZhbiAow5pjKSAoMTk4NykuIEThu68gbGnhu4d1IG7DoHkgZ+G7k20gaMahbiAzNzAwIHRyxrDhu51uZyBo4bujcCB24bubaSBt4bulYyB0acOqdSBuZ2hpw6puIGPhu6l1IChnaeG6oyDEkeG7i25oKSBsw6AgeMOieSBk4buxbmcgbeG7mXQgbcO0IGjDrG5oIGNo4bqpbiDEkW/DoW4gYuG7h25oIFN1eSBHacOhcCAoSHlwb3RoeXJvaWQpIGThu7FhIHbDoG8gdGjDtG5nIHRpbiBn4buTbSA2IGJp4bq/biBz4buRIGxpw6puIHThu6VjIDogVHXhu5VpIHbDoCBnacOhIHRy4buLIGPDoWMgYmlvbWFya2VyIG5oxrAgaG9ybW9uZSBUMywgVFQ0LCBUNFUgdsOgIEZUSSwgdsOgICAxMiBiaeG6v24gbmjhu4sgcGjDom4gZ+G7k20gR2nhu5tpIHTDrW5oLCDEkWnhu4F1IHRy4buLIHRoeXJveGluZSwgY8OzIHRoYWksIHBo4bqrdSB0aHXhuq10IHR1eeG6v24gZ2nDoXAsIHN1eSB0dXnhur9uIG7Do28gdGjDuXkgKGh5cG9waXR1aXRhcnkgKSwgdHJp4buHdSBjaOG7qW5nIGLhu6l1IChnb2l0cmUsdHVtb3IpLHTDom0gbMO9IChwc3ljaCksIMSRaeG7gXUgdHLhu4sgSTEzMSwgbGl0aGl1bS4gUGjDom4gbG/huqFpIGLhu4duaCBuaMaw4bujYyBnacOhcCB0cm9uZyBk4buvIGxp4buHdSBuZ3V5w6puIHRo4buneSBjw7MgxJHhur9uIDUgbmjDo24ga+G6v3QgcXXhuqMgbMOgIE5lZ2F0aXZlLCBoeXBvdGh5cm9pZCwgcHJpbWFyeSBoeXBvdGh5cm9pZCwgY29tcGVuc2F0ZWQgaHlwb3RoeXJvaWQgdsOgIHNlY29uZGFyeSBoeXBvdGh5cm9pZC4gxJDhu4MgxJHGoW4gZ2nhuqNuIGjDs2EsIHRhIHPhu60gZOG7pW5nIHBoacOqbiBi4bqjbiBnaeG6o24gbMaw4bujYyBj4bunYSBRdWFuIFN1biB0csOqbiB0aMawIHZp4buHbiBPcGVuTUwgdHJvbmcgxJHDsyBiaeG6v24ga+G6v3QgcXXhuqMgxJHGsOG7o2MgZ2nhuqNuIGzGsOG7o2MgY2jhu4kgY8OybiAyIG5ow6NuIChuaOG7iyBwaMOibikgbMOgIFBvc2l0aXZlIHbDoCBOZWdhdGl2ZS4NCg0KTMawdSDDvTogVHJvbmcgZOG7ryBsaeG7h3UgZ+G7kWMgY8OzIDEgdHLGsOG7nW5nIGjhu6NwIG3DoCBiaeG6v24gQWdlIGLhu4sgbmjhuq1wIGxp4buHdSBzYWksIHbhu5tpIGdpw6EgdHLhu4sgNDQ1IHR14buVaSx0YSBsb+G6oWkgdHLGsOG7nW5nIGjhu6NwIG7DoHkga2jhu49pIGThu68gbGnhu4d1IHBow6JuIHTDrWNoLg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KDQpkZj1yZWFkLmNzdigiaHR0cHM6Ly93d3cub3Blbm1sLm9yZy9kYXRhL2dldF9jc3YvNTM1MzQvaHlwb3RoeXJvaWQuYXJmZiIsbmEuc3RyaW5ncyA9ICI/IikNCg0KDQpkZj1kZiU+JWRwbHlyOjpzZWxlY3QoYWdlLHNleCxwcmVnbmFudCwNCiAgICAgICAgICAgICAgIG9uLnRoeXJveGluZSxxdWVyeS5vbi50aHlyb3hpbmUsDQogICAgICAgICAgICAgICBvbi5hbnRpdGh5cm9pZC5tZWRpY2F0aW9uLA0KICAgICAgICAgICAgICAgdGh5cm9pZC5zdXJnZXJ5LA0KICAgICAgICAgICAgICAgSTEzMS50cmVhdG1lbnQsc2ljaywNCiAgICAgICAgICAgICAgIGxpdGhpdW0sZ29pdHJlLHR1bW9yLGh5cG9waXR1aXRhcnkscHN5Y2gsDQogICAgICAgICAgICAgICBUU0gsVDMsVFQ0LFQ0VSxGVEksYmluYXJ5Q2xhc3MpDQoNCmRmPWRmJT4lZmlsdGVyKC4sYWdlPDEwMCkNCg0Kc3RyKGRmKQ0KYGBgDQoNCsSQ4bqndSB0acOqbiwgdGEgcGjDom4gY2hpYSBuZ+G6q3Ugbmhpw6puIGThu68gbGnhu4d1IGfhu5FjIHRow6BuaCAyIHBo4bqnbiwgbeG7mXQgZMO5bmcgY2hvIHZp4buHYyBodeG6pW4gbHV54buHbiBtw7QgaMOsbmgsIG3hu5l0IGTDuW5nIMSR4buDIGtp4buDbSDEkeG7i25oIG3DtCBow6xuaCBuw6B5LiBDaMO6bmcgdGEga2jDtG5nIMSR4bulbmcgxJHhur9uIHRlc3RzZXQgY2hvIMSR4bq/biBraGkga2nhu4NtIMSR4buLbmguDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShjYXJldCkNCg0Kc2V0LnNlZWQoMTIzNCkNCmlkVHJhaW49Y2FyZXQ6OmNyZWF0ZURhdGFQYXJ0aXRpb24oeT1kZiRiaW5hcnlDbGFzcywgcD0wLjUsbGlzdD1GQUxTRSkNCg0KdHJhaW5zZXQ9ZGZbaWRUcmFpbixdDQp0ZXN0c2V0PWRmWy1pZFRyYWluLF0NCmBgYA0KDQpUaeG6v3AgdGhlbyB0YSBz4bq9IHRoxINtIGTDsiDEkeG6t2MgdMOtbmggcGjDom4gYuG7kSBj4bunYSBk4buvIGxp4buHdSBi4bqxbmcgYmnhu4N1IMSR4buTLiDEkMOieSBsw6AgY8O0bmcgxJFv4bqhbiDEkeG6p3UgdGnDqm4gdHJvbmcgbeG7l2kgbmdoacOqbiBj4bupdSwgbmjGsG5nIMSR4buRaSB24bubaSBOYcOvdmUgQmF5ZXMgdGjDrCBow6xuaCDhuqNuaCB0cuG7sWMgcXVhbiBjw7JuIGPDsyDDvSBuZ2jEqWEgxJHhurdjIGJp4buHdCwgdsOsIG5o4buvbmcgZ8OsIGLhuqFuIG5ow6xuIHRo4bqleSB2w6AgY+G6o20gbmjhuq1uIGzDumMgbsOgeSBjxaluZyBjaMOtbmggbMOgIG5o4buvbmcgZ8OsIG3DoCBi4bqhbiBz4bq9IHRodSDEkcaw4bujYyB0cm9uZyBu4buZaSBkdW5nIG3DtCBow6xuaC4gDQoNCkRvIE5CIGtow7RuZyB4w6l0IGxpw6puIGjhu4cgdsOgIHTGsMahbmcgdMOhYyBnaeG7r2EgY8OhYyBiaeG6v24sIGNow7puZyB0YSBjaOG7iSBxdWFuIHTDom0gxJHhur9uIDIgxJFp4buBdTogdMOtbmggdMawxqFuZyBwaOG6o24gZ2nhu69hIGPDoWMgcGjDom4gbOG7m3AgdsOgIMSR4bq3YyB0w61uaCBwaMOibiBi4buRIGPhu6dhIGJp4bq/biBsacOqbiB04bulYy4gDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KdHJhaW5zZXRbLC1jKDI6MTQpXSU+JWdhdGhlcihhZ2U6RlRJLGtleT0iRmVhdHVyZXMiLHZhbHVlPSJTY29yZSIpJT4lDQogIGdncGxvdCgpKw0KICBnZW9tX2RlbnNpdHkoYWVzKHg9U2NvcmUsZmlsbD1iaW5hcnlDbGFzcyksYWxwaGE9MC41KSsNCiAgZmFjZXRfd3JhcCh+RmVhdHVyZXMsbmNvbD0yLHNjYWxlcyA9ICJmcmVlIikrDQogIHRoZW1lX2J3KCkrc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoImJsdWUiLCJyZWQiKSkNCg0KdHJhaW5zZXRbLC1jKDI6MTQpXSU+JWdhdGhlcihhZ2U6RlRJLGtleT0iRmVhdHVyZXMiLHZhbHVlPSJTY29yZSIpJT4lDQogIGdncGxvdCgpKw0KICBnZW9tX3Zpb2xpbihhZXMoeD1iaW5hcnlDbGFzcyx5PVNjb3JlLGZpbGw9YmluYXJ5Q2xhc3MpLGFscGhhPTAuNSkrDQogIGNvb3JkX2ZsaXAoKSsNCiAgZmFjZXRfd3JhcCh+RmVhdHVyZXMsbmNvbD0yLHNjYWxlcyA9ICJmcmVlIikrDQogIHRoZW1lX2J3KCkrc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoImJsdWUiLCJyZWQiKSkNCg0KdHJhaW5zZXRbLGMoMjoxNCwyMCldJT4lZ2F0aGVyKHNleDpwc3ljaCxrZXk9IkZlYXR1cmVzIix2YWx1ZT0iTGV2ZWwiKSU+JW5hLm9taXQoKSU+JQ0KICBnZ3Bsb3QoKSsNCiAgZ2VvbV9iYXIoYWVzKHg9TGV2ZWwseT0uLmNvdW50Li4sZmlsbD1iaW5hcnlDbGFzcyksYWxwaGE9MC43KSsNCiAgY29vcmRfZmxpcCgpKw0KICBmYWNldF93cmFwKH5GZWF0dXJlcyxuY29sPTMsc2NhbGVzID0gImZyZWUiKSsNCiAgdGhlbWVfYncoOCkrc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoImJsdWUiLCJyZWQiKSkNCmBgYA0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnBsb3RmdW5jTG93IDwtIGZ1bmN0aW9uKGRhdGEsbWFwcGluZyl7DQogIHAgPC0gZ2dwbG90KGRhdGEgPSBkYXRhLG1hcHBpbmc9bWFwcGluZykrDQogICAgc3RhdF9kZW5zaXR5MmQoZ2VvbT0icG9seWdvbiIsYWVzKGZpbGw9dHJhaW5zZXQkYmluYXJ5Q2xhc3MsYWxwaGEgPSAuLmxldmVsLi4pKSsNCiAgICBnZW9tX3BvaW50KGFlcyhmaWxsPXRyYWluc2V0JGJpbmFyeUNsYXNzLGNvbD10cmFpbnNldCRiaW5hcnlDbGFzcyksc2hhcGU9Ii4iKSsNCiAgICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiYmx1ZSIsInJlZCIpKSsNCiAgICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzPWMoImJsdWU0IiwicmVkNCIpKSsNCiAgICB0aGVtZV9idygpDQogIHANCn0NCg0KcGxvdGRlbnNmdW5jIDwtIGZ1bmN0aW9uKGRhdGEsbWFwcGluZyl7DQogIHAgPC0gZ2dwbG90KGRhdGEgPSBkYXRhLG1hcHBpbmc9bWFwcGluZykrDQogICAgc3RhdF9kZW5zaXR5KGFlcyhmaWxsPXRyYWluc2V0JGJpbmFyeUNsYXNzLGFscGhhID0gMC43KSkrDQogICAgZ2VvbV9ydWcoYWVzKGNvbD10cmFpbnNldCRiaW5hcnlDbGFzcykpKw0KICAgIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1jKCJibHVlIiwicmVkIikpKw0KICAgIHNjYWxlX2NvbG9yX21hbnVhbCh2YWx1ZXM9YygiYmx1ZTQiLCJyZWQ0IikpKw0KICAgIHRoZW1lX2J3KCkNCiAgcA0KfQ0KDQpsaWJyYXJ5KEdHYWxseSkNCg0KZ2dwYWlycyh0cmFpbnNldFssLWMoMjoxNCwyMCldLGxvd2VyID0gbGlzdChjb250aW51b3VzPXBsb3RmdW5jTG93KSwNCiAgICAgICAgZGlhZz1saXN0KGNvbnRpbnVvdXM9cGxvdGRlbnNmdW5jKSkNCmBgYA0KDQpE4buxYSB2w6BvIGPDoWMgYmnhu4N1IMSR4buTLCB0YSBjw7MgdGjhu4MgcGjDoW4gxJFvw6FuIGzDoDogc+G7sSB0xrDGoW5nIHBo4bqjbiBs4bubbiBuaOG6pXQgZ2nhu69hIDIgcGjDom4gbOG7m3AgUC9OIMSRxrDhu6NjIGJp4buDdSBoaeG7h24gdOG6oWkgY8OhYyBiaeG6v24gVDRVLCBUVDQsRlRJLCBn4bujaSDDvSDEkcOieSBsw6Agbmjhu69uZyBiaeG6v24gc+G7kSBo4buvdSBoaeG7h3UgdHJvbmcgbcO0IGjDrG5oLk5nb+G6oWkgdHLhu6sgYmnhur9uIEFnZSB2w6AgVFNILCBjw6FjIHRy4buLIHPhu5EgaG9ybW9uZSBjw7JuIGzhuqFpIGPDsyBow6xuaCBk4bqhbmcgcGjDom4gYuG7kSB0xrDGoW5nIMSR4buRaSBiw6xuaCB0aMaw4budbmcuIEPDoWMgYmnhur9uIG5o4buLIHBow6JuIGPDsyBnacOhIHRy4buLIEZhbHNlIGjhuqd1IGjhur90IMSR4buBdSBjw7MgbGnDqm4gaOG7hyB24bubaSBwaMOibiBsb+G6oWkgUG9zaXRpdmUgY+G7p2EgYuG7h25oIE5oxrDhu6NjIGdpw6FwLiBEbyBjw7Mgc+G7sSBt4bqldCBjw6JuIGLhurFuZyBs4bubbiB0cm9uZyBjw6FjIGJp4bq/biDEkeG7i25oIHTDrW5oLCDEkWnhu4NuIGjDrG5oIGzDoCBiaeG6v24gaHlwb3BpdHVpdGFyeSB0aOG6rW0gY2jDrSBraMO0bmcgY8OzIHRyxrDhu51uZyBo4bujcCBuw6BvIHRodeG7mWMgbG/huqFpIFQsIGRvIMSRw7MgdGEgY8OzIHRo4buDIHBo4bqjaSBkw7luZyDEkeG6v24gTGFwbGFjZSBzbW9vdGhpbmcuDQoNCiMgUGFja2FnZSBlMTA3MQ0KDQpNw7QgaMOsbmggTmFpdmUgQmF5ZXMgdHJvbmcgUiBjw7MgdGjhu4MgxJHGsOG7o2MgZOG7sW5nIGLhurFuZyAyIHBhY2thZ2VzLCDEkeG6p3UgdGnDqm4gbMOgIGUxMDcxLiANCg0KaMOgbSBuYWl2ZUJheWVzIGPhu6dhIHBhY2thZ2UgZTEwNzEgZOG7sW5nIG3DtCBow6xuaCBOYWl2ZSBCYXllcyB24bubaSB04buRYyDEkeG7mSBy4bqldCBuaGFuaC5Ucm9uZyB0w7l5IGNo4buJbmgsIHRhIGtoYWkgYsOhbyBuYS5vbWl0IMSR4buDIHjhu60gbMO9IHRyxrDhu51uZyBo4bujcCB0aGnhur91IHPDs3QgZOG7ryBsaeG7h3UgcmnDqm5nIGNobyB04burbmcgYmnhur9uLCB2w6AgbGFwbGFjZT0xLg0KDQpO4buZaSBkdW5nIGPhu6dhIG3DtCBow6xuaCBjaG8gdGjhuqV5IHjDoWMgc3XhuqV0IHRp4buBbiBuZ2hp4buHbSBjaG8gbmjDo24gZ2nDoSB0cuG7iyBQIGzDoCAwLjkxNSB2w6AgTiBsw6AgMC4wODUuIFNhdSDEkcOzIGzDoCBt4buZdCBsb+G6oXQgY8OhYyBi4bqjbmcgY2jDqW8gdHLDrG5oIGLDoHkgeMOhYyBzdeG6pXQgxJFp4buBdSBraeG7h24gdOG7q25nIGdpw6EgdHLhu4sgY+G7p2EgYmnhur9uIMSR4bqndSB2w6BvIHRyb25nIMSRaeG7gXUga2nhu4duIG3hu5dpIG5ow6NuIGdpw6EgdHLhu4sgY+G7p2EgYmnhur9uIMSRw61jaCBu4bq/dSDEkeG6p3UgdsOgbyBsw6AgbeG7mXQgYmnhur9uIHLhu51pIHLhuqFjIGhheSBuaOG7iyBwaMOibiAodGjDrSBk4bulIFNleCksIGhv4bq3YyBUcnVuZyBiw6xuaCBjaG8gYmnhur9uIMSR4bqndSB2w6BvIGxpw6puIHThu6VjLg0KDQpNw7QgaMOsbmggTkJtb2QxIG7DoHkgcuG6pXQgbmjhuqF5LCBuaMawbmcgY2jGsGEgxJHhurdjIGhp4buHdSwgbsOzIHbhuqtuIGPDsm4gY2jhuqluIMSRb8OhbiBzYWkgdHJvbmcgbeG7mXQgc+G7kSB0csaw4budbmcgaOG7o3AgTmVnYXRpdmUuDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShlMTA3MSkNCg0KTkJtb2QxPWUxMDcxOjpuYWl2ZUJheWVzKGZvcm11bGE9YmluYXJ5Q2xhc3N+LiwNCiAgICAgICAgICAgICAgICAgICAgICAgICBkYXRhPXRyYWluc2V0LA0KICAgICAgICAgICAgICAgICAgICAgICAgIG5hLmFjdGlvbiA9IG5hLm9taXQsbGFwbGFjZT0xKQ0KDQpOQm1vZDENCg0KcHJlZE5CMT1wcmVkaWN0KE5CbW9kMSx0ZXN0c2V0KQ0KDQpjYXJldDo6Y29uZnVzaW9uTWF0cml4KHJlZmVyZW5jZT10ZXN0c2V0JGJpbmFyeUNsYXNzLA0KICAgICAgICAgICAgICAgICAgICAgICBkYXRhPXByZWROQjEsDQogICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aXZlPSJQIiwNCiAgICAgICAgICAgICAgICAgICAgICAgbW9kZT0iZXZlcnl0aGluZyIpDQpgYGANCg0KIyBQYWNrYWdlIGtsYVINCg0KTeG7mXQgcGFja2FnZSBraMOhYyBjaG8gcGjDqXAgZOG7sW5nIG3DtCBow6xuaCBOYWl2ZSBCYXllcyDEkcOzIGzDoCBrbGFSLiANCg0KTcO0IGjDrG5oIE5CbW9kMiBj4bunYSBrbGFSIGPDsyBu4buZaSBkdW5nIHbDoCBoaeG7h3UgbsSDbmcgdMawxqFuZyB04buxIG5oxrAgbcO0IGjDrG5oIE5CbW9kMSBj4bunYSBlMTA3MS4NCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpsaWJyYXJ5KGtsYVIpDQoNCk5CbW9kMj1OYWl2ZUJheWVzKGZvcm11bGE9YmluYXJ5Q2xhc3N+LiwNCiAgICAgICAgICAgICAgICAgIGRhdGE9dHJhaW5zZXQsbmEuYWN0aW9uPW5hLm9taXQsZkw9MSwNCiAgICAgICAgICAgICAgICAgIHVzZWtlcm5lbD1GKQ0KDQpOQm1vZDIkdGFibGVzDQoNCnByZWROQjI9cHJlZGljdChOQm1vZDIsdGVzdHNldCkNCg0KY2FyZXQ6OmNvbmZ1c2lvbk1hdHJpeChyZWZlcmVuY2U9dGVzdHNldCRiaW5hcnlDbGFzcywNCiAgICAgICAgICAgICAgICAgICAgICAgZGF0YT1wcmVkTkIyJGNsYXNzLA0KICAgICAgICAgICAgICAgICAgICAgICBwb3NpdGl2ZT0iUCIsDQogICAgICAgICAgICAgICAgICAgICAgIG1vZGU9ImV2ZXJ5dGhpbmciKQ0KYGBgDQoNCkzGsHUgw70gbMOgIGPhuqMgMiBtw7QgaMOsbmggbsOgeSDEkeG7gXUgZMO5bmcgZ2nhuqMgxJHhu4tuaCBwaMOibiBi4buRIEdhdXNzaWFuIGNobyB04bqldCBj4bqjIGJp4bq/biBsacOqbiB04bulYy4gVGEgY8OzIHRo4buDIHbhur0gYmnhu4N1IMSR4buTIMSR4buDIHF1YW4gc8OhdCBoaeG7h24gdMaw4bujbmcgY8OhYyBiaeG6v24gbGnDqm4gdOG7pWMgxJHDoyBi4buLIMOhcCDEkeG6t3QgcGjDom4gYuG7kSBHYXVzc2lhbiBuaMawIHRo4bq/IG7DoG86IMSRw6J5IGzDoCBow6xuaCDhuqNuaCBob8OgbiB0b8OgbiBraMOhYyBzbyB24bubaSB0aOG7sWMgdOG6vyA6DQoNCmBgYHtyfQ0KcGFyKG1mcm93PWMoMiwzKSkNCg0KcGxvdCgwOjAuMSwgeGxpbT1jKDAsOTgpLHlsaW09YygwLDAuMDMpLA0KICAgICB5bGFiPSJkZW5zaXR5Iix0eXBlPSJuIiwgDQogICAgIHhsYWI9ImFnZSIpDQpjdXJ2ZShkbm9ybSh4LCBOQm1vZDIkdGFibGVzJGFnZVsxLDFdLCBOQm1vZDIkdGFibGVzJGFnZVsxLDJdKSwgYWRkPVRSVUUsIGNvbD0iYmx1ZSIpDQpjdXJ2ZShkbm9ybSh4LCBOQm1vZDIkdGFibGVzJGFnZVsyLDFdLCBOQm1vZDIkdGFibGVzJGFnZVsyLDJdKSwgYWRkPVRSVUUsIGNvbD0icmVkIikNCg0KcGxvdCgwOjMwMCwgeGxpbT1jKDAsMzAwKSx5bGltPWMoMCwwLjA5KSwNCiAgICAgeWxhYj0iZGVuc2l0eSIsdHlwZT0ibiIsIA0KICAgICB4bGFiPSJUU0giKQ0KY3VydmUoZG5vcm0oeCwgTkJtb2QyJHRhYmxlcyRUU0hbMSwxXSwgTkJtb2QyJHRhYmxlcyRUU0hbMSwyXSksIGFkZD1UUlVFLCBjb2w9ImJsdWUiKQ0KY3VydmUoZG5vcm0oeCwgTkJtb2QyJHRhYmxlcyRUU0hbMiwxXSwgTkJtb2QyJHRhYmxlcyRUU0hbMiwyXSksIGFkZD1UUlVFLCBjb2w9InJlZCIpDQoNCnBsb3QoMDo2LCB4bGltPWMoMCw2KSx5bGltPWMoMDoxLjUpLA0KICAgICB5bGFiPSJkZW5zaXR5Iix0eXBlPSJuIiwgDQogICAgIHhsYWI9IlQzIikNCmN1cnZlKGRub3JtKHgsIE5CbW9kMiR0YWJsZXMkVDNbMSwxXSwgTkJtb2QyJHRhYmxlcyRUM1sxLDJdKSwgYWRkPVRSVUUsIGNvbD0iYmx1ZSIpDQpjdXJ2ZShkbm9ybSh4LCBOQm1vZDIkdGFibGVzJFQzWzIsMV0sIE5CbW9kMiR0YWJsZXMkVDNbMiwyXSksIGFkZD1UUlVFLCBjb2w9InJlZCIpDQoNCnBsb3QoMDo2LCB4bGltPWMoMCw2KSx5bGltPWMoMCwzKSwNCiAgICAgeWxhYj0iZGVuc2l0eSIsdHlwZT0ibiIsIA0KICAgICB4bGFiPSJUNFUiKQ0KY3VydmUoZG5vcm0oeCwgTkJtb2QyJHRhYmxlcyRUNFVbMSwxXSwgTkJtb2QyJHRhYmxlcyRUNFVbMSwyXSksIGFkZD1UUlVFLCBjb2w9ImJsdWUiKQ0KY3VydmUoZG5vcm0oeCwgTkJtb2QyJHRhYmxlcyRUNFVbMiwxXSwgTkJtb2QyJHRhYmxlcyRUNFVbMiwyXSksIGFkZD1UUlVFLCBjb2w9InJlZCIpDQoNCnBsb3QoMDo0MDAseGxpbT1jKDAsNDAwKSx5bGltPWMoMCwwLjAxNSksDQogICAgIHlsYWI9ImRlbnNpdHkiLHR5cGU9Im4iLCANCiAgICAgeGxhYj0iVFQ0IikNCmN1cnZlKGRub3JtKHgsIE5CbW9kMiR0YWJsZXMkVFQ0WzEsMV0sIE5CbW9kMiR0YWJsZXMkVFQ0WzEsMl0pLCBhZGQ9VFJVRSwgY29sPSJibHVlIikNCmN1cnZlKGRub3JtKHgsIE5CbW9kMiR0YWJsZXMkVFQ0WzIsMV0sIE5CbW9kMiR0YWJsZXMkVFQ0WzIsMl0pLCBhZGQ9VFJVRSwgY29sPSJyZWQiKQ0KDQpwbG90KDA6MzAwLHhsaW09YygwLDMwMCkseWxpbT1jKDAsMC4wMTUpLA0KICAgICB5bGFiPSJkZW5zaXR5Iix0eXBlPSJuIiwgDQogICAgIHhsYWI9IkZUSSIpDQpjdXJ2ZShkbm9ybSh4LCBOQm1vZDIkdGFibGVzJEZUSVsxLDFdLCBOQm1vZDIkdGFibGVzJEZUSVsxLDJdKSwgYWRkPVRSVUUsIGNvbD0iYmx1ZSIpDQpjdXJ2ZShkbm9ybSh4LCBOQm1vZDIkdGFibGVzJEZUSVsyLDFdLCBOQm1vZDIkdGFibGVzJEZUSVsyLDJdKSwgYWRkPVRSVUUsIGNvbD0icmVkIikNCg0KYGBgDQoNCkLDonkgZ2nhu50gY2jDum5nIHRhIHRo4buxYyBoaeG7h24gbeG7mXQgbcO0IGjDrG5oIE5CIGtow6FjIHRyb25nIGtsYVIsIG5oxrBuZyBs4bqnbiBuw6B5IGPDsyDDoXAgZOG7pW5nIHBoxrDGoW5nIHBow6FwIGtlcm5lbCDEkeG7gyBraOG6o28gc8OhdCBjaMOtbmggeMOhYyBoxqFuIHBow6JuIGLhu5EgY8OhYyBiaeG6v24gbGnDqm4gdOG7pWMuDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KTkJtb2Qyaz1rbGFSOjpOYWl2ZUJheWVzKGZvcm11bGE9YmluYXJ5Q2xhc3N+LiwNCiAgICAgICAgICAgICAgICAgICAgICAgIGRhdGE9dHJhaW5zZXQsDQogICAgICAgICAgICAgICAgICAgICAgICBuYS5hY3Rpb24gPSBuYS5vbWl0LGZMPTEsDQogICAgICAgICAgICAgICAgICAgICAgICB1c2VrZXJuZWw9VCkNCg0KcHJlZE5CMms9cHJlZGljdChOQm1vZDJrLHRlc3RzZXQpDQoNCmNhcmV0Ojpjb25mdXNpb25NYXRyaXgocmVmZXJlbmNlPXRlc3RzZXQkYmluYXJ5Q2xhc3MsDQogICAgICAgICAgICAgICAgICAgICAgIGRhdGE9cHJlZE5CMmskY2xhc3MsDQogICAgICAgICAgICAgICAgICAgICAgIHBvc2l0aXZlPSJQIiwNCiAgICAgICAgICAgICAgICAgICAgICAgbW9kZT0iZXZlcnl0aGluZyIpDQpgYGANCg0KTcO0IGjDrG5oIE5CbW9kMmsgY8OzIHPhu60gZOG7pW5nIGtlcm5lbCB04buRdCBoxqFuIHNvIHbhu5tpIG3DtCBow6xuaCBOQm1vZDIgdsOgIDE7IGtoaSBraeG7g20gxJHhu4tuaCB0csOqbiB0ZXN0c2V0LCB0YSB0aOG6pXkgbcO0IGjDrG5oIE5CbW9kMmsgY8OzIMSR4buZIGNow61uaCB4w6FjIHNhdSBjw6JuIGLhurFuZyBjYW8gaMahbiByw7UgcuG7h3Q6IEJBQz0wLjg2IHNvIHbhu5tpIDAuNzAsIG3hu6ljIMSR4buZIHTGsMahbmcgaOG7o3AgdGjhu7FjIHThur8gdsOgIGNo4bqpbiDEkW/DoW4gcuG6pXQgY2FvIHbhu5tpIEthcHBhPTAuNzAuIE3DtCBow6xuaCBuw6B5IHLhuqV0IG5o4bqheSB2w6Aga2jDoSDEkeG6t2MgaGnhu4d1LiANCg0KS2hpIGTDuW5nIHBoxrDGoW5nIHBow6FwIGtlcm5lbCwgcGjDom4gYuG7kSBjw6FjIGJp4bq/biBsacOqbiB04bulYyBjw7MgZOG6oW5nIG5oxrAgc2F1Og0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCg0KTkJtb2QzPU5haXZlQmF5ZXMoZm9ybXVsYT1iaW5hcnlDbGFzc34uLA0KICAgICAgICAgICAgICAgICBkYXRhPXRyYWluc2V0WywtYygyOjE0KV0sDQogICAgICAgICAgICAgICAgIG5hLmFjdGlvbiA9IG5hLm9taXQsZkw9MSwNCiAgICAgICAgICAgICAgICAgdXNla2VybmVsPVQpDQoNCnBhcihtZnJvdz1jKDIsMykpDQoNCnBsb3QoTkJtb2QzLGxlZ2VuZHBsb3Q9VCxjb2w9YygiYmx1ZSIsInJlZCIpLGx0eT1jKDEsMSkpDQpgYGANCg0KTGnDqm4gaOG7hyBnaeG7r2EgbeG7l2kgYmnhur9uIGxpw6puIHThu6VjIMSR4bqndSB2w6BvIHbDoCB4w6FjIHN14bqldCDEkWnhu4F1IGtp4buHbiBjaG8gbmjDo24gUCwgTiDEkcaw4bujYyBraOG6o28gc8OhdCBi4bqxbmcgYmnhu4N1IMSR4buTIHNhdToNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQp0ZXN0ZGY8LXRlc3RzZXQlPiVtdXRhdGUoTmVnYXRpdmU9cHJlZE5CMmskcG9zdGVyaW9yWywxXSwNCiAgICAgICAgICAgICAgICAgICAgICAgUG9zaXRpdmU9cHJlZE5CMmskcG9zdGVyaW9yWywyXSwNCiAgICAgICAgICAgICAgICAgICAgICAgTGFiZWw9cHJlZE5CMmskY2xhc3MpDQoNCnRlc3RkZlssLWMoMjoxNCldJT4lZ2F0aGVyKGFnZTpGVEksa2V5PSJGZWF0dXJlcyIsdmFsdWU9IlNjb3JlIiklPiVuYS5vbWl0KCklPiUNCiAgZ2F0aGVyKE5lZ2F0aXZlLFBvc2l0aXZlLGtleT0iVHlwZSIsdmFsdWU9IlBvc3RlcmlvciIpJT4lDQogIGdncGxvdCgpKw0KICBnZW9tX3Ntb290aChhZXMoeD1TY29yZSx5PVBvc3RlcmlvciwNCiAgICAgICAgICAgICAgICAgIGNvbD1UeXBlLGZpbGw9VHlwZSksDQogICAgICAgICAgICAgIG1ldGhvZD0ibG9lc3MiLGFscGhhPTAuMikrDQogIGZhY2V0X3dyYXAofkZlYXR1cmVzLG5jb2w9MixzY2FsZXMgPSAiZnJlZSIpKw0KICB0aGVtZV9idygpKw0KICBzY2FsZV9jb2xvcl9tYW51YWwodmFsdWVzPWMoImJsdWUiLCJyZWQiKSkrDQogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1jKCJibHVlIiwicmVkIikpDQpgYGANCg0KQ8OybiDEkcOieSBsw6AgaGnhu4d1IG7Eg25nIGPhu6dhIG3DtCBow6xuaCBOQm1vZDJrDQoNCmBgYHtyfQ0KdGVzdGRmWywtYygyOjE0KV0lPiVnYXRoZXIoYWdlOkZUSSxrZXk9IkZlYXR1cmVzIix2YWx1ZT0iU2NvcmUiKSU+JW5hLm9taXQoKSU+JQ0KICBtdXRhdGUoLixBY2N1cmFjeT1pZl9lbHNlKGJpbmFyeUNsYXNzPT1MYWJlbCwiQ29ycmVjdCIsIkZhaWxlZCIpKSU+JQ0KICBnZ3Bsb3QoKSsNCiAgZ2VvbV9iYXIoYWVzKHg9YmluYXJ5Q2xhc3MseT0uLmNvdW50Li4sZmlsbD1BY2N1cmFjeSkscG9zaXRpb249ImZpbGwiLGFscGhhPTAuOCkrDQogIHRoZW1lX2J3KCkrY29vcmRfZmxpcCgpKw0Kc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoImJsdWUiLCJyZWQiKSkNCmBgYA0KDQpUYSBsw6BtIHRow6ptIDIgYmnhu4N1IMSR4buTIGtow6FjIMSR4buDIMSRw6FuaCBnacOhIHbhu4EgbeG7qWMgcXVhbiB0cuG7jW5nIGPhu6dhIHThu6tuZyBiaeG6v24gbGnDqm4gdOG7pWMgdsOgIG5o4buLIHBow6JuOg0KDQpgYGB7cn0NCm51bWRmeT1kYXRhLmZyYW1lKGFnZT1OQm1vZDJrJHRhYmxlcyRhZ2UkUCR5LA0KICAgICAgICAgICAgICAgICAgVFNIPU5CbW9kMmskdGFibGVzJFRTSCRQJHksDQogICAgICAgICAgICAgICAgICBUMz1OQm1vZDJrJHRhYmxlcyRUMyRQJHksDQogICAgICAgICAgICAgICAgICBUVDQ9TkJtb2QyayR0YWJsZXMkVFQ0JFAkeSwNCiAgICAgICAgICAgICAgICAgIFQ0VT1OQm1vZDJrJHRhYmxlcyRUNFUkUCR5LA0KICAgICAgICAgICAgICAgICAgRlRJPU5CbW9kMmskdGFibGVzJEZUSSRQJHkpJT4lZ2F0aGVyKGFnZTpGVEksa2V5PSJGZWF0dXJlcyIsdmFsdWU9IkRlbnNpdHkiKQ0KDQpjYXRkZj1kYXRhLmZyYW1lKFByZWdGPU5CbW9kMmskdGFibGVzJHByZWduYW50WzIsMV0sDQogICAgICAgICAgICAgICAgIFRoeXJveEY9TkJtb2QyayR0YWJsZXMkb24udGh5cm94aW5lWzIsMV0sDQogICAgICAgICAgICAgICAgIFF1ZXJ5Rj1OQm1vZDJrJHRhYmxlcyRxdWVyeS5vbi50aHlyb3hpbmVbMiwxXSwNCiAgICAgICAgICAgICAgICAgTWVkRj1OQm1vZDJrJHRhYmxlcyRvbi5hbnRpdGh5cm9pZC5tZWRpY2F0aW9uWzIsMV0sDQogICAgICAgICAgICAgICAgIFN1cmdGPU5CbW9kMmskdGFibGVzJHRoeXJvaWQuc3VyZ2VyeVsyLDFdLA0KICAgICAgICAgICAgICAgICBJMTMxRj1OQm1vZDJrJHRhYmxlcyRJMTMxLnRyZWF0bWVudFsyLDFdLA0KICAgICAgICAgICAgICAgICBTaWNrRj1OQm1vZDJrJHRhYmxlcyRzaWNrWzIsMV0sDQogICAgICAgICAgICAgICAgIEdvaXRyZUY9TkJtb2QyayR0YWJsZXMkZ29pdHJlWzIsMV0sDQogICAgICAgICAgICAgICAgIExpdGhpdW1GPU5CbW9kMmskdGFibGVzJGxpdGhpdW1bMiwxXSwNCiAgICAgICAgICAgICAgICAgVHVtb3JGPU5CbW9kMmskdGFibGVzJHR1bW9yWzIsMV0sDQogICAgICAgICAgICAgICAgIEh5cG9QaXRGPU5CbW9kMmskdGFibGVzJGh5cG9waXR1aXRhcnlbMiwxXSwNCiAgICAgICAgICAgICAgICAgUHN5Y2hGPU5CbW9kMmskdGFibGVzJHBzeWNoWzIsMV0pDQoNCmBgYA0KDQpgYGB7cn0NCm51bWRmeSU+JQ0KICBnZ3Bsb3QoYWVzKHg9cmVvcmRlcihGZWF0dXJlcyxEZW5zaXR5KSx5PURlbnNpdHksZmlsbD1yZW9yZGVyKEZlYXR1cmVzLERlbnNpdHkpKSkrDQogIGdlb21fYm94cGxvdChzaG93LmxlZ2VuZCA9IEYpKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzPU5VTEwsIkNvbnRyaWJ1dGlvbiBsZXZlbCIpKw0KICBzY2FsZV94X2Rpc2NyZXRlKCJGZWF0dXJlcyIpKw0KICBjb29yZF9mbGlwKCkrdGhlbWVfYncoKQ0KDQpjYXRkZiU+JWdhdGhlcihQcmVnRjpQc3ljaEYsa2V5PSJGZWF0dXJlIix2YWx1ZT0iUG9zdGVyaW9yIiklPiUNCiAgZ2dwbG90KGFlcyh4PXJlb3JkZXIoRmVhdHVyZSxQb3N0ZXJpb3IpLHk9UG9zdGVyaW9yLGZpbGw9cmVvcmRlcihGZWF0dXJlLFBvc3RlcmlvcikpKSsNCiAgZ2VvbV90ZXh0KGFlcyhsYWJlbD1GZWF0dXJlKSxudWRnZV94ID0gMC4xLG51ZGdlX3kgPSAwLjAxKSsNCiAgZ2VvbV9wb2ludChzaGFwZT0yMSxjb2w9ImJsYWNrIixzaXplPTUsc2hvdy5sZWdlbmQgPSBGKSsNCiAgdGhlbWVfYncoKSsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscz1OVUxMLCJDb250cmlidXRpb24gbGV2ZWwiKSsNCiAgc2NhbGVfeF9kaXNjcmV0ZShsYWJlbHM9TlVMTCwiRmVhdHVyZXMiKQ0KYGBgDQoNCiMgxq91IHRo4bq/IHbDoCBHaeG7m2kgaOG6oW4NCg0KU2F1IMSRw6J5IGzDoCBuaOG7r25nIMawdSB0aOG6vyB2w6AgbmjGsOG7o2MgxJFp4buDbSBj4bunYSBnaeG6o2kgdGh14bqtdCBOYcOvdmUgQmF5ZXMga2hpIMOhcCBk4bulbmcgY2hvIG5naGnDqm4gY+G7qXUgeSBo4buNYyA6DQoNCiMjIMavdSB0aOG6vw0KDQpDxqEgY2jhur8gaG/huqF0IMSR4buZbmcgdsOgIGvhur90IHF14bqjIG3DtCBow6xuaCBk4buxbmcgYuG6sW5nIE5haXZlIEJheWVzIHTGsMahbmcgxJHhu5NuZyB24bubaSBxdXkgdHLDrG5oIHN1eSBsdeG6rW4gdHJvbmcgdGjhu7FjIGjDoG5oIGzDom0gc8OgbmcuIFRo4bqtdCB24bqteSwgcXV5IHRyw6xuaCBjaOG6qW4gxJFvw6FuIG3DoCBi4bqhbiDEkWFuZyBsw6BtIGjhurFuZyBuZ8OgeSB0csOqbiBt4buXaSBi4buHbmggbmjDom4gbMOgIHPhu7EgdOG7lW5nIGjhu6NwIGPhu6dhIGjDoG5nIGxv4bqhdCBzdXkgbHXhuq1uIEJheWVzIHRoZW8ga2nhu4N1IDogVuG7m2kgZOG7ryBsaeG7h3UgbMOibSBzw6BuZyB0cm9uZyBi4buHbmggw6FuICh0aeG7gW4gc+G7rSwgYuG7h25oIHPhu60sIHRyaeG7h3UgY2jhu6luZyBjaOG7qWMgbsSDbmcvdGjhu7FjIHRo4buDLCBr4bq/dCBxdeG6oyB4w6l0IG5naGnhu4dt4oCmKSB0aMOsIHjDoWMgc3XhuqV0IG3huq9jIGLhu4duaCBj4bunYSBi4buHbmggbmjDom4gbMOgIGJhbyBuaGnDqnUgPyANCg0KIVtdKE5haXZlQmF5ZXNmaWcyLnBuZykNCg0KVuG7gSBt4bq3dCBr4bu5IHRodeG6rXQgTkIgY8OzIG5oaeG7gXUgxrB1IHRo4bq/OiBuw7Mga2hhaSBwaMOhIGThu68gbGnhu4d1IHbhu5tpIHThu5FjIMSR4buZIGPhu7FjIGvDrCBuaGFuaCwgdsOsIDIgbMO9IGRvIOKAkyB0aOG7qSBuaOG6pXQgTkIgeMOpdCByacOqbmcgbOG6uyB04burbmcgYmnhur9uIG5oxrBuZyBraMO0bmcgY+G6p24gYmnhur90IG3hu5FpIGxpw6puIGjhu4csIHThu5UgaOG7o3AgZ2nhu69hIGNow7puZywgdGjhu6kgaGFpIHbDrCB04bqhaSBt4buXaSBiaeG6v24sIHjDoWMgc3XhuqV0IMSRaeG7gXUga2nhu4duIHJpw6puZyBwaOG6p24gxJHGsOG7o2MgxrDhu5tjIGzGsOG7o25nIMSRxqFuIGdp4bqjbiBi4bqxbmcgcGjDqXAgxJHhur9tIHThuqduIHN14bqldCBob+G6t2MgZ2nhuqMgxJHhu4tuaCBwaMOibiBwaOG7kWkgY2h14bqpbi4gVMawxqFuZyB04buxLCBraGkgdGhpIGjDoG5oIG5oaeG7h20gduG7pSB0csOqbiBk4buvIGxp4buHdSBt4bubaSB0aMOsIE5haXZlIEJheWUgY8WpbmcgY+G7sWMga8OsIG5oYW5oLiBRdeG6oyB0aOG7sWMsIGhp4bq/bSBnaeG6o2kgdGh14bqtdCBuw6BvIGPDsyB04buRYyDEkeG7mSBuaGFuaCB0cm9uZyBj4bqjIDIgcXXDoSB0csOsbmg6IGjhu41jIHThu6sgZOG7ryBsaeG7h3UgdsOgIHRoaSBow6BuaCBuaGnhu4dtIHbhu6UgbmjGsCBOYWl2ZSBCYXllcy4gRG8gxJHDsywgTkIgcuG6pXQgdGjDrWNoIGjhu6NwIGNobyBuaOG7r25nIGLhu5kgZOG7ryBsaeG7h3Uga8OtY2ggdGjGsOG7m2MgbOG7m24sIGPhuqMgduG7gSBz4buRIGzGsOG7o25nIGJp4bq/biB2w6Agc+G7kSB0csaw4budbmcgaOG7o3AuDQoNCk3hu5l0IMawdSB0aOG6vyBoaeG7g24gbmhpw6puIGtow6FjIGzDoCB0w61uaCBwaOG7lSBxdcOhdDogTmFpdmUgQmF5ZXMgY2jhuqVwIG5o4bqtbiB04bqldCBj4bqjIGPDoWMgbG/huqFpIGJp4bq/biB0cm9uZyBk4buvIGxp4buHdSDEkeG6p3UgdsOgbywgdOG7qyBsacOqbiB04bulYywgcuG7nWkgcuG6oWMgY2hvIMSR4bq/biBuaOG7iyBwaMOibi4gDQoNCk3hurd0IGtow6FjLCBOQiBjaOG7iSBj4bqnbiBj4buhIG3huqt1IHbhu6thIMSR4bunIGNobyBt4buXaSBiaeG6v24sIHbDrCB0aOG7sWMgY2jhuqV0IG7DsyBraMO0bmcgZMO5bmcgaOG6v3QgdG/DoG4gYuG7mSB04burbmcgdHLGsOG7nW5nIGjhu6NwIG3DoCBjaOG7iSBxdWFuIHTDom0gxJHhur9uIHThu4kgbOG7hyBwaMOibiBi4buRIGNobyBt4buXaSBi4bqtYyBnacOhIHRy4buLIChiaeG6v24gcuG7nWkgcuG6oWMpIGhv4bq3YyDEkeG6t2MgdMOtbmggcGjDom4gcGjhu5FpIChiaeG6v24gbGnDqm4gdOG7pWMpLiBDxaluZyB2w6wgbMO9IGRvIG7DoHkgTkIgw610IG5o4bqheSBj4bqjbSB24bubaSBuaGnhu4V1IHbDoCBjaOG6pXAgbmjhuq1uIGThu68gbGnhu4d1IGLhu4sgdGhp4bq/dSBzw7N0IHLhuqNpIHLDoWMgY2hvIHThu6tuZyBiaeG6v24uIFRyb25nIHRo4buxYyBow6BuaCBsw6JtIHPDoG5nIGtow7RuZyBwaOG6o2kgbMO6YyBuw6BvIHRhIGPFqW5nIHRodSB0aOG6rXAgxJHGsOG7o2MgxJHhuqd5IMSR4bunIHRow7RuZyB0aW4sIG3hu5l0IG3DtCBow6xuaCBjaOG6pXAgbmjhuq1uIHRoaeG6v3Ugc8OzdCBk4buvIGxp4buHdSBuaMawICBOYWl2ZSBCYXllcyBjw7MgdGjhu4Mgc+G6vSBjw7Mgw61jaCB0cm9uZyB0csaw4budbmcgaOG7o3AgbsOgeS4NCg0KVmnhu4djIGThu7FuZyBtw7QgaMOsbmggTkIgdsO0IGPDuW5nIMSRxqFuIGdp4bqjbi4gTeG7mXQgbeG6t3QsIGRvIGdp4bqjIMSR4buLbmggduG7gSB0w61uaCDEkeG7mWMgbOG6rXAsIG5o4buvbmcgYmnhur9uIHPhu5EgdsO0IGhp4buHdSwg4bqjbmggaMaw4bufbmcgcuG6pXQgw610IMSR4bq/biBr4bq/dCBxdeG6oyBj4bunYSBtw7QgaMOsbmgsIHRyb25nIGtoaSBjw6FjIGJp4bq/biBz4buRIHF1YW4gdHLhu41uZyB24bqrbiB0aOG7sWMgaGnhu4duIMSR4buZYyBs4bqtcCB2YWkgdHLDsiBj4bunYSBtw6xuaC4gxJBp4buBdSBuw6B5IGPDsyBuZ2jEqWEgbMOgIGtoaSBkw7luZyBOQiwgYuG6oW4ga2jDtG5nIGPhuqduIHF1YW4gdMOibSDEkeG6v24gdmnhu4djIGNo4buNbiBs4buNYyBiaeG6v24gc+G7kSB2w6AgbeG6pXQgdGjhu51pIGdpYW4geMOpdCBjw6FjIGdp4bqjIHRodXnhur90IHbhu4EgdMawxqFuZyB0w6FjIMSRYSBjaGnhu4F1LCBtw6AgY2jhu4kgY+G6p24gY2h1eeG7g24gdG/DoG4gYuG7mSBk4buvIGxp4buHdSDEkeG6p3UgdsOgbyBjaG8gTkIgdOG7sSBsby4gQuG6oW4gY8WpbmcgY8OzIHRo4buDIGTDuW5nIE5CIHbDtCB0xrAgY2hvIG5o4buvbmcgYuG7mSBk4buvIGxp4buHdSBjw7MgcuG6pXQgbmhp4buBdSBiaeG6v24gdsOgIGNoxrBhIGPDsyBi4bqldCBj4bupIMO9IHTGsOG7n25nIG7DoG8gcsO1IHLDoG5nIHbhu4EgdmFpIHRyw7IgdOG7q25nIGJp4bq/biBjxaluZyBuaMawIGxpw6puIGjhu4cgZ2nhu69hIGNow7puZy4NCg0KTuG7mWkgZHVuZyBtw7QgaMOsbmggTmFpdmUgQmF5ZXMgY8WpbmcgcuG6pXQgxJHGoW4gZ2nhuqNuIHbDoCBk4buFIGhp4buDdSwgbsOzIGfhuqduIGdp4buRbmcgbmjGsCBt4buZdCBwaMOibiB0w61jaCB0aOG7kW5nIGvDqiDEkcahbiBiaeG6v24gaMOgbmcgbG/huqF0Lg0KDQpO4bq/dSBt4bulYyB0acOqdSBj4bunYSBuZ2hpw6puIGPhu6l1IGzDoCBwaMOibiBsb+G6oWkgY2jDrW5oIHjDoWMsIE5haXZlIEJheWVzIGPDsyBoaeG7h3UgcXXhuqMgbeG7mXQgY8OhY2ggxJHDoW5nIG5n4bqhYyBuaGnDqm4uIFR1eSDEkcahbiBnaeG6o24gKHbDoCBwaGkgbMO9KSwgbmjGsG5nIGhp4buHdSBuxINuZyBj4bunYSBtw7QgaMOsbmggTkIga2jDtG5nIHRodWEga8OpbSBi4bqldCBj4bupIGdp4bqjaSB0aHXhuq10IG7DoG8ga2jDoWMsIGvhu4MgY+G6oyBuaOG7r25nIHBoxrDGoW5nIHBow6FwIHBo4bupYyB04bqhcCBoxqFuIHLhuqV0IG5oaeG7gXUuIMSQ4buZIGNow61uaCB4w6FjIGPhu6dhIE5haXZlIEJheWVzIHThu6tuZyDEkcaw4bujYyBjaOG7qW5nIG1pbmggdHLDqm4gdGjhu7FjIHThur8sIHRo4bqtbSBjaMOtIGNobyBuaOG7r25nIGLDoGkgdG/DoW4gcGjDom4gbG/huqFpIHThu5tpIDEwLTIwIG5ow6NuIGdpw6EgdHLhu4suIA0KDQojIyBI4bqhbiBjaOG6vw0KDQpC4bqldCBs4bujaSBs4bubbiBuaOG6pXQgY+G7p2EgTmFpdmUgQmF5ZXMgY2jDrW5oIGzDoCBz4buxIMSRxqFuIGdp4bqjbiBxdcOhIG3hu6ljIGPhu6dhIG7Dsy4gR2nhuqMgxJHhu4tuaCB24buBIHTDrW5oIMSR4buZYyBs4bqtcCB0dXnhu4d0IMSR4buRaSBnaeG7r2EgY8OhYyBiaeG6v24gxJHhuqd1IHbDoG8gbMOgIHLhuqV0IHbDtCBsw70gdsOgIGhvw6BuIHRvw6BuIG3DonUgdGh14bqrbiB24bubaSBjxqEgY2jhur8gc2luaCBsw70gYuG7h25oLiBEbyDEkcOzIG3DtCBow6xuaCBOYWl2ZSBCYXllcyB0aMaw4budbmcga2jDtG5nIGNobyBwaMOpcCBkaeG7hW4gZ2nhuqNpIHbhu4EgdMawxqFuZyB0w6FjIMSRYSBjaGnhu4F1IGhv4bq3YyBraGFpIHBow6Egbmjhu69uZyBjxqEgY2jhur8gc2luaCBsw70gYuG7h25oIGjhu41jIG3hu5tpLg0KDQpN4buZdCBuaMaw4bujYyDEkWnhu4NtIGtow6FjIGPhu6dhIE5haXZlIEJheWVzIMSRw7MgbMOgIG7DsyBuaOG6oXkgY+G6o20gduG7m2kgduG6pW4gxJHhu4EgbeG6pXQgY8OibiBi4bqxbmcgZ2nhu69hIGPDoWMgbmjDo24gcGjDom4gbG/huqFpIHRyb25nIGThu68gbGnhu4d1LiBUdXkgbmhpw6puIMSRw6J5IGNo4buJIGzDoCB24bqlbiDEkeG7gSBr4bu5IHRodeG6rXQgdsOgIGNow7puZyB0YSBjw7MgdGjhu4Mga2jhuq9jIHBo4bulYyBuw7MuDQoNCiMgS+G6v3QgbHXhuq1uDQoNCk5haXZlIEJheWVzIGzDoCBt4buZdCBnaeG6o2kgdGh14bqtdCBNYWNoaW5lIGxlYXJuaW5nIHR1eSDEkcahbiBnaeG6o24gbmjGsG5nIHLhuqV0IGhp4buHdSBxdeG6oyBraGkgw6FwIGThu6VuZyBjaG8gYsOgaSB0b8OhbiBwaMOibiBsb+G6oWkuIEPGoSBjaOG6vyBob+G6oXQgxJHhu5luZyBj4bunYSBOYWl2ZSBCYXllcyBn4bqnbiBuaMawIHTGsMahbmcgxJHhu5NuZyB24bubaSBiaeG7h24gbHXhuq1uIGNo4bqpbiDEkW/DoW4gdHLDqm4gbMOibSBzw6BuZywgbmdv4bqhaSB0cuG7qyBnaeG6oyDEkeG7i25oIHBoaSBsw70gduG7gSB0w61uaCDEkeG7mWMgbOG6rXAgZ2nhu69hIGPDoWMgdHJp4buHdSBjaOG7qW5nIHbDoCBiaW9tYXJrZXIuIFR1eSBuaGnDqm4gc28gduG7m2kgbmjhu69uZyBwaMawxqFuZyBwaMOhcCB0aOG7kW5nIGvDqiBj4buVIMSRaeG7g24ga2jDoWMgbmjGsCBraeG7g20gxJHhu4tuaCBLaSBiw6xuaCBwaMawxqFuZywgdCB0ZXN0LCBr4bq/dCBxdeG6oyBj4bunYSBOYWl2ZSBCYXllIGN1bmcgY+G6pXAgbmhp4buBdSB0aMO0bmcgdGluIGjGoW4sIG5oxrAgeMOhYyBzdeG6pXQgxJFp4buBdSBraeG7h24gY2hvIHThu6tuZyBuaMOjbiBnacOhIHRy4buLLCBjw7Mgc+G7rSBk4bulbmcgeMOhYyBzdeG6pXQgdGnhu4FuIMSR4buLbmguIE5haXZlIEJheWVzIGPDsm4gY2hvIHBow6lwIHThu5VuZyBo4bujcCB0aMO0bmcgdGluIHThu6sgcGjDom4gdMOtY2ggxJHGoW4gYmnhur9uIHRow6BuaCBxdXkgbHXhuq10IGNo4bqpbiDEkW/DoW4sIHbDoCBxdXkgbHXhuq10IG7DoHkgcuG6pXQgaGnhu4d1IHF14bqjIG5oxrAgdGEgdGjhuqV5LiANCg==