1 Đặt vấn đề

Trong năm 2017, sự ra đời của package keras đã cho phép người dùng R có thể thực hành được một phương pháp mô hình thuộc loại tân tiến nhất, đó là mạng thần kinh nhân tạo sâu (Deep neural network hay Deep learning) với TensofFlow (trước đây bạn phải dùng Python). Package keras cho phép R kết nối với TensorFlow back-end của Python để dựng mô hình Deep ANN với nhiều ứng dụng hữu ích như hồi quy, xếp loại nhị phân hay đa giá trị, computer vision (convolutional Neural network), dự báo chuỗi thời gian, mô hình tự sinh mẫu, vv

Ở bài trước, Nhi đã trình bày 1 ứng dụng Hồi quy điển hình với keras. Trong bài này, Nhi sẽ tiếp tục giới thiệu một mô hình phân loại với kết quả nhị phân (Binary Classification).Trong bài, Nhi sẽ thực hiện tuần tự từng bước của một quy trình mẫu mực cho nghiên cứu ứng dụng Machine learning. Khi thực sự bắt tay vào làm nghiên cứu bạn cũng sẽ phải đi qua những bước này, bao gồm:

  • Biện luận lựa chọn Algorithm

  • Thăm dò dữ liệu

  • Hoán chuyển, sơ chế dữ liệu

  • Huấn luyện mô hình

  • Kiểm định mô hình

  • Biện luận, giải nghĩa mô hình

Một mục tiêu khác của bài này, đó là giúp cho các bạn làm quen với một phong cách viết R code cũng như quy trình phân tích dữ liệu hoàn toàn mới lạ bằng cách ứng dụng những package mới nhất của 2 tác giả Hadley Wickham và Max Kuhn, như recipes (sơ chế dữ liệu), rsample (tái chọn mẫu), dplyr (thao tác dữ liệu và tóm tắt), và các toán tử pipe trong tidyverse.

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.

2 Bước 1: Thăm dò dữ liệu

library(tidyverse)
## -- Attaching packages ---------------------------------------------------------------------- tidyverse 1.2.1 --
## v ggplot2 2.2.1     v purrr   0.2.4
## v tibble  1.4.2     v dplyr   0.7.5
## 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("https://www.openml.org/data/get_csv/53534/hypothyroid.csv",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)%>%na.omit()

colnames(df)=c("AGE","SEX","PREG","ON_THY","QO_THY","ON_AT","SURG",
               "I131","SICK","LIT","GOIT","TUMOR","HPI","PSY",
               "TSH","T3","TT4","T4U","FTI","CLASS")

str(df)
## 'data.frame':    2642 obs. of  20 variables:
##  $ AGE   : int  41 70 80 66 68 84 71 59 28 63 ...
##  $ SEX   : Factor w/ 2 levels "F","M": 1 1 1 1 2 1 1 1 2 1 ...
##  $ PREG  : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ ON_THY: Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ QO_THY: Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ ON_AT : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ SURG  : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ I131  : 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 2 1 1 1 ...
##  $ LIT   : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ GOIT  : 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 2 1 2 1 1 1 1 ...
##  $ HPI   : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ PSY   : Factor w/ 2 levels "f","t": 1 1 1 1 1 1 1 1 1 1 ...
##  $ TSH   : num  1.3 0.72 2.2 0.6 2.4 1.1 0.03 2.8 3.3 1.5 ...
##  $ T3    : num  2.5 1.2 0.6 2.2 1.6 2.2 3.8 1.7 1.8 1.2 ...
##  $ TT4   : num  125 61 80 123 83 115 171 97 109 117 ...
##  $ T4U   : num  1.14 0.87 0.7 0.93 0.89 0.95 1.13 0.91 0.91 0.96 ...
##  $ FTI   : num  109 70 115 132 93 121 151 107 119 121 ...
##  $ CLASS : Factor w/ 2 levels "N","P": 2 2 2 2 2 2 2 2 2 2 ...
##  - attr(*, "na.action")= 'omit' Named int  2 3 4 6 7 12 16 17 21 24 ...
##   ..- attr(*, "names")= chr  "2" "3" "4" "6" ...

Như vậy, cấu trúc dữ liệu gồm có: 1 biến kết quả nhị phân là CLASS, 13 features là biến định tính nhị phân, 6 biến định lượng liên tục. Dữ liệu gồm 2642 trường hợp

table(df$CLASS)
## 
##    N    P 
##  216 2426

Tổng cộng có 2426 trường hợp Positive và 216 trường hợp Negative. Như vậy có sự bất xứng về nhãn kết quả, gợi ý rằng trong quá trình tái chọn mẫu và phân chia, ta phải cố gắng bảo toàn tỉ lệ giữa 2 nhãn P và N.

Điều đầu tiên Nhi sẽ làm, đó là cắt ngẫu nhiên dữ liệu gốc thành 2 phần bằng nhau (do ta có quá nhiều dữ liệu, nên tỉ lệ 50/50 là lý tưởng). Có thể làm việc này chỉ bằng 3 dòng code với hàm initial_split của gói rsample.

Với tùy chỉnh strata cho biến CLASS, ta có thể bảo toàn tỉ lệ giữa 2 nhãn P/N như nhau cho 2 tập trainset và testset là 1213/108

dat=df

# Prior Spliting 

library(rsample)
## Loading required package: broom
## 
## Attaching package: 'rsample'
## The following object is masked from 'package:tidyr':
## 
##     fill
set.seed(2405)

data_split <- initial_split(dat, strata = "CLASS",prop=0.5)

trainset <- training(data_split)
testset <- testing(data_split)

table(trainset$CLASS)
## 
##    N    P 
##  108 1213
table(testset$CLASS)
## 
##    N    P 
##  108 1213

Ta sẽ không nhìn vào tập testset, vì đây là dữ liệu dùng để kiểm định mô hình, nhưng ta có thể khảo sát tập trainset. Nhi sẽ dùng phương pháp trực quan để đánh giá phân bố của các biến và so sánh giữa 2 phân nhóm P/N

# First Exploration

trainset%>%gather(AGE,TSH,T3,TT4,T4U,FTI,key="Feature",value="Score")%>%
  ggplot(aes(x=Score,fill=CLASS))+
  geom_density(alpha=0.5,col="black")+
  theme_bw()+
  facet_wrap(~Feature,scales="free",ncol=3)+
  scale_fill_manual(values=c("blue","red"))

trainset%>%gather(PREG:PSY,key="Feature",value="Status")%>%
  ggplot(aes(x=Status,fill=CLASS))+
  geom_bar(stat="count",position="fill",alpha=0.5,col="black")+
  scale_y_continuous(labels=NULL)+
  theme_bw()+
  facet_wrap(~Feature,scales="free",ncol=6)+
  scale_fill_manual(values=c("blue","red"))

Ở hình thứ nhất, ta có thể nhận định rằng hầu hết biến định lượng có phân bố bất thường, không đồng dạng và không cân đối. Điều này gợi ý rằng một phép hoán chuyển để hiệu chỉnh Skewness (thí dụ Box-Cox) cần được áp dụng. Ngoài ra, do ta sẽ dùng ANN, và giải thuật này chỉ hoạt động tối ưu khi dữ liệu đầu vào được chuẩn hóa về cùng thang đo, nên ta sẽ áp dụng Centering và Scale cho toàn bộ biến định lượng.

Hình thứ hai cho thấy sự tương phản của các biến định tính trong dữ liệu là rất yếu, thậm chí gợi ý rằng ta phải loại bỏ một số biến không cho thấy sự tương phản rõ như I131, SICK.

3 Bước 2: Chuẩn bị dữ liệu

Bước tiếp theo sẽ là hoán chuyển dữ liệu, cũng như quá trình chuẩn bị nguyên liệu phù hợp với món ăn mà ta muốn nấu. Để làm việc này, ta sẽ dùng package recipes để tạo ra 1 công thức nấu ăn có nội dung như sau:

  1. Đầu tiên, lấy toàn bộ features ,

  2. Sau đó, làm hoán chuyển Box-Cox cho toàn bộ những biến định lượng

  3. Tiếp theo, chuẩn hóa toàn bộ biến định lượng

  4. Sau đó loại bỏ những biến định lượng tương quan mạnh với nhau, và chỉ giữ lại những biến không có tương quan

  5. Tiếp theo, chuyển những biến định tính thành dummy variable có giá trị 0/1

  6. Cuối cùng, loại bỏ tất cả những biến nhị phân lẫn định lượng nào có variance rất thấp (=0 hay gần =0).

Quy trình liên hoàn trên đây có vẻ phức tạp, nhưng khi dùng recipes mọi chuyện hết sức dễ dàng, bạn chỉ việc đặt bước này sau bước kia và xâu chuỗi bằng pipes, tận cùng bằng hàm prep.

Khi áp dụng công thức này trên trainset, ta sẽ thu được tập dữ liệu dùng để dựng mô hình,

Tương tự, khi áp dụng cho testset, ta sẽ thu được dữ liệu dùng để kiểm định mô hình

# recipes

library(recipes)
## 
## Attaching package: 'recipes'
## The following object is masked from 'package:stringr':
## 
##     fixed
## The following object is masked from 'package:stats':
## 
##     step
rec_obj<-recipe(CLASS~., data = trainset)%>%
  step_BoxCox(all_numeric(), -all_outcomes())%>%
  step_center(all_numeric(),-all_outcomes())%>%
  step_scale(all_numeric(),-all_outcomes())%>%
  step_corr(all_numeric(),-all_outcomes())%>%
  step_dummy(all_nominal(),-all_outcomes())%>%
  step_zv(all_predictors(),-all_outcomes())%>%
  step_nzv(all_predictors(),-all_outcomes())%>%
  prep(training = trainset)

train_df <- bake(rec_obj, newdata = trainset)
test_df <- bake(rec_obj, newdata = testset)

str(train_df)
## Classes 'tbl_df', 'tbl' and 'data.frame':    1321 obs. of  10 variables:
##  $ AGE     : num  0.9242 1.4924 0.9806 0.3102 0.0902 ...
##  $ TSH     : num  -0.302 0.366 -1.938 0.518 -1.006 ...
##  $ T3      : num  -1.007 -2.065 2.071 -0.287 -0.154 ...
##  $ TT4     : num  -1.375 -0.766 1.745 -0.255 0.783 ...
##  $ T4U     : num  -0.633 -1.903 0.77 -0.383 0.236 ...
##  $ FTI     : num  -1.2407 0.2277 1.3602 -0.0284 0.7348 ...
##  $ CLASS   : Factor w/ 2 levels "N","P": 2 2 2 2 2 2 2 2 2 2 ...
##  $ SEX_M   : num  0 0 0 0 1 0 0 0 0 0 ...
##  $ ON_THY_t: num  0 0 0 0 0 0 0 0 0 0 ...
##  $ PSY_t   : num  0 0 0 0 0 0 0 0 0 0 ...

Như vậy, sau khi sơ chế ta đã loại bỏ được hầu hết biến định tính có phương sai quá thấp và chuẩn hóa hoàn toàn biến định lượng

train_df%>%
  gather(AGE:FTI,key="Feature",value="Score")%>%
  ggplot(aes(x=Score,fill=CLASS))+
  geom_density(alpha=0.5,col="black")+
  theme_bw()+
  facet_wrap(~Feature,scales="free",ncol=3)+
  scale_fill_manual(values=c("blue","red"))

train_df%>%
  gather(SEX_M:PSY_t,key="Feature",value="Status")%>%
  ggplot(aes(x=factor(Status),fill=CLASS))+
  geom_bar(stat="count",position="fill",alpha=0.5,col="black")+
  scale_y_continuous(labels=NULL)+
  theme_bw()+
  facet_wrap(~Feature,scales="free",ncol=6)+
  scale_fill_manual(values=c("blue","red"))

Ta hy vọng với dữ liệu này, mô hình sẽ hoạt động tối ưu :

4 Bước 3: Xây dựng cấu trúc mạng neuron

Trước khi lựa chọn một giải thuật (algorithm) Machine learning cho nghiên cứu của mình, ta cần biện luận về cơ sở của lựa chọn này. Trong thí dụ này, Nhi phải giải thích tại sao lại dùng một mô hình blackbox thay vì hồi quy logistic ?

Mô hình Neural network phù hợp trong 2 trường hợp :

  • Một bệnh lý mà triệu chứng lâm sàng, rối loạn sinh lý, sinh hóa đã được mô tả một cách kinh điển, đầy đủ trong y văn và không còn gì phải bàn cãi nữa, tuy nhiên mục tiêu của nghiên cứu không phải là diễn dịch/phân tích, nhưng nhằm thiết lập một công cụ tự động (software) hỗ trợ bác sĩ đưa ra quyết định chẩn đoán trong những trường hợp khó.

  • Dữ liệu đầu vào có bản chất phức tạp, vượt ra khỏi quy tắc thông thường của môn Thống kê, thí dụ hình ảnh (ảnh chụp nội soi, giải phẫu bệnh, MRI, …), âm thanh (tiếng tim, âm hô hấp), tín hiệu điện (ECG, EEG), văn bản (chữ viết tay, bệnh án điện tử)…, vì Deep learning cho phép phân tích rất tốt những loại dữ liệu này.

Thí dụ hiện thời thuộc trường hợp thứ nhất: Mục tiêu giả định là thiết lập một quy luật chẩn đoán tự động, càng chính xác càng tốt nhưng không quan tâm đến liên hệ giữa các biến trong dữ liệu.

Trước hết, ta trích xuất 2 vectors của biến CLASS sau khi chuyển nó thành biến nhị phân (0/1, cho tập train_df và test_df)

train_label <- ifelse(pull(train_df, CLASS) == "P", 1, 0)

test_label <- ifelse(pull(test_df, CLASS) == "P", 1, 0)

Tiếp theo, ta dựng cấu trúc (architecture) của mạng neuron:

Đây là một mạng ANN gồm 3 lớp, 1 lớp đầu vào gồm 9 perceptron để tiếp nhận 9 features. Tín hiệu trọng số sẽ được kích hoạt bằng hàm relu , sau đó đi qua 3 lớp ẩn, gồm 1 lớp ẩn thứ nhất có 64 neuron,liên kết bão hoàn với đầu vào, sau đó tín hiệu được lọc lần thứ nhất bằng 1 lớp drop_out với tỉ lệ 0.01, tiếp tục đi qua lớp ẩn thứ 2 có 32 neuron, rồi 1 lớp dropout thứ hai trước khi kết thúc ở lớp cuối cùng chỉ có 1 neuron để xuất kết quả, kích hoạt bằng hàm sigmoid.

Mạng ANN này sau đó được compile với tiêu chí huấn luyện là binary_accuracy, hàm loss là binary_crossentropy và optimizer là hàm “adam”

5 Bước 5: Huấn luyện mô hình

Ta chạy thử cấu trúc này với 50 lượt lấy mẫu với kích thước 100 trường hợp và kiểm định trên 30%

# ANN

library(keras)

build_model <- function(){
  model_keras <- keras_model_sequential()%>% 
    # 1st hidden layer
    layer_dense(
      units              = 64, 
      kernel_initializer = "uniform", 
      activation         = "relu", 
      input_shape        = ncol(train_df)-1) %>% 
    # 1st Dropout 
    layer_dropout(rate = 0.01) %>%
    # 2nd hidden layer
    layer_dense(
      units              = 32, 
      kernel_initializer = "uniform", 
      activation         = "relu") %>% 
    # Dropout 
    layer_dropout(rate = 0.01) %>%
    #  Output layer
    layer_dense(
      units              = 1, 
      kernel_initializer = "uniform", 
      activation         = "sigmoid") %>% 
    # Compile ANN
    compile(
      optimizer = 'adam',
      loss      = 'binary_crossentropy',
      metrics   = c('binary_accuracy')
    )
}

model<-build_model()

fit_keras <- fit(
  object=model,
  x=as.matrix(train_df[,-7]), 
  y=train_label,
  batch_size       = 100,
  epochs           = 50, 
  validation_split = 0.3,
  verbose = 0
)

fit_keras
## Trained on 924 samples, validated on 397 samples (batch_size=100, epochs=50)
## Final epoch (plot to see history):
##            val_loss: 0.04597
## val_binary_accuracy: 0.9849
##                loss: 0.04735
##     binary_accuracy: 0.9848

Kết quả huấn luyện rất khả quan, accuracy đã được tối ưu đến 98.59% và loss hạ xuống còn 0.04

plot(fit_keras)+theme_bw()

6 Bước 4: Kiểm chứng chéo

Tuy nhiên, phẩm chất của mô hình còn tùy thuộc vào nhiều tham số trong quá trình huấn luyện, thí dụ đơn giản nhất ở đây là số lượt (epoch), Nhi muốn biết liệu tăng epoch cao hơn có làm thay đổi hiệu quả của mô hình ANN hay không ?

Để có câu trả lời, Nhi sẽ thực hiện một quy trình kiểm chứng chéo 10 blocks, mỗi 9 block như vậysẽ dùng để huấn luyện, Nhi sẽ thử lần lượt giá trị epoch tăng dần từ 20,40,60,80,100,120 và kiểm định hiệu năng mô hình trên 1 block còn lại

Nhi viết 1 hàm tương đối “khủng” với nội dung như sau:

  1. Khai báo giá trị epoch, danh sách tái chọn mẫu split, và seed number.

  2. Sau mỗi lần chạy, xóa bộ nhớ của keras để chuẩn bị cho lần chạy tiếp theo

  3. Dựng 1 mạng neuron có cấu trúc như trên

  4. Từ danh sách chọn mẫu, trích xuất 2 matrices tập train, test và vector label

  5. Huấn luyện ANN sử dụng 9 blocks

  6. Phân loại 1 block (test) dưới dạng probability (0:1) và dán nhãn P/N

  7. Tính tất cả (xin nhấn mạnh: tất cả) tiêu chí phẩm chất của model vừa dựng, bao gồm 4 nhóm: tính hữu dụng, tính chính xác, nguy cơ sai lầm, sự tương hợp

(Xin xem bài này để hiểu rõ hơn: http://rpubs.com/lengockhanhi/347941 )

  1. Xuất kết quả kiểm định
library(yardstick)
## 
## Attaching package: 'yardstick'
## The following object is masked from 'package:readr':
## 
##     spec
library(scoring)
library(pROC)
## Type 'citation("pROC")' for a citation.
## 
## Attaching package: 'pROC'
## The following objects are masked from 'package:stats':
## 
##     cov, smooth, var
library(rsample)

# A large Cross validation function


large_cv_func<- function(epoch, split, seed, ...) {
  # Set the seed to get reproducible starting values and dropouts
  set.seed(seed)
  # clears memory used by the last trial  
  on.exit(keras::backend()$clear_session())

  # Define ANN
  
  model<- keras_model_sequential()%>% 
    # 1st hidden layer
    layer_dense(
      units              = 64, 
      kernel_initializer = "uniform", 
      activation         = "relu", 
      input_shape        = ncol(train_df)-1) %>% 
    # 1st Dropout 
    layer_dropout(rate = 0.01) %>%
    # 2nd hidden layer
    layer_dense(
      units              = 32, 
      kernel_initializer = "uniform", 
      activation         = "relu") %>% 
    # Dropout 
    layer_dropout(rate = 0.01) %>%
    #  Output layer
    layer_dense(
      units              = 1, 
      kernel_initializer = "uniform", 
      activation         = "sigmoid") %>% 
    # Compile ANN
    compile(
      optimizer = 'adam',
      loss      = 'binary_crossentropy',
      metrics   = c('binary_accuracy')
    )  
  # data used for training ("analysis" set)
  block_df<- analysis(split)

  block_lab<-ifelse(pull(block_df,CLASS)=="P",1,0)
  
  model%>%fit(
    x=as.matrix(block_df[,-7]),
    y=block_lab,
    epochs = epoch, 
    ...
  )
  
  # Now obtain the holdout set for prediction
  blocktest_df<- assessment(split)
  
  blocktest_lab<-ifelse(pull(blocktest_df,CLASS)=="P",1,0)
  
  
  Pred_class_vector<- 
    predict_classes(object = model, 
                    x = as.matrix(blocktest_df[,-7]))%>%as.vector()
  
  Pred_Prob_vector<- 
    predict_proba(object = model, 
                  x = as.matrix(blocktest_df[,-7]))%>%as.vector()
  
  valid_df <- tibble(
    truth      = as.factor(blocktest_lab) %>% 
      fct_recode (Positive = "1", Negative = "0"),
    estimate   = as.factor(Pred_class_vector) %>% 
      fct_recode (Positive = "1", Negative = "0"),
    class_prob = Pred_Prob_vector )
  
  dfpred<-valid_df%>%mutate(Truth=.$truth,
                    BinTruth=if_else(.$truth=="P",1,0),
                    ClassNeg=1-.$class_prob,
                    ClassPos=.$class_prob,
                    ClassLab=.$estimate)
  
  TP=with(dfpred,sum(ClassLab==Truth & ClassLab=="Positive"))
  TN=with(dfpred,sum(ClassLab==Truth & ClassLab!="Positive"))
  FN=with(dfpred,sum(ClassLab!=Truth & Truth=="Positive"))
  FP=with(dfpred,sum(ClassLab!=Truth & Truth!="Positive"))
  
  TPR=TP/(TP+FN)
  TNR=TN/(TN+FP)
  SEN=TPR
  SPEC=TNR
  Recall=SEN
  
  ACC=(TP+TN)/(TP+FP+TN+FN)
  BAC=(TPR+TNR)/2
  PPV=TP/(TP+FP)
  NPV=TN/(TN+FN)
  Precision=PPV
  G=sqrt(Recall*Precision)
  F2=(1+2^2)*(Precision*Recall)/((2^2)*Precision+Recall)
  F0.5=(1+(0.5)^2)*(Precision*Recall)/((0.5^2)*Precision+Recall)
  F1=2*TP/(TP+TP+FP+FN)
  Fscore=(1+1)*(Precision*Recall)/(1*Precision+Recall)
  
  FNR=FN/(FN+TP)
  FPR=FP/(FP+TN)
  FDR=FP/(TP+FP)
  FOR=1-NPV
  BER=with(dfpred,mean((ClassPos>0.5 & BinTruth==0) | (ClassPos<0.5 & BinTruth==1)))
  logloss=with(dfpred,-mean(log10(ClassPos)))
  LSR=with(dfpred,mean(log10(ClassPos)))
  mmce=with(dfpred,mean(Truth!=ClassLab))
  
  brier= dfpred%>%scoring::brierscore(data=.,BinTruth~ClassPos)%>%mean()
  
  SSR=dfpred%>%
    scoring::sphscore(data=.,BinTruth~ClassPos)%>%mean()
  

  BM=TPR+TNR-1
  MK=PPV+NPV-1
  MCC=(TP*TN-FP*FN)/(sqrt((TP+FP)*(TP+FN)*(TN+FP)*(TN+FN)))
  Kappa=psych::cohen.kappa(as.matrix(dfpred[,c(1,2)]))%>%.$weighted.kappa
  
  rbind(BM,MK,MCC,Kappa)
  
  LRpos=SEN/(1-SPEC)
  LRneg=(1-SEN)/SPEC
  
  
  myroc <- roc(Truth~ClassPos, data=dfpred)
  
  AUC=myroc$auc[1]
  
  return(cbind(
    TP,TN,FN,FP,
    TPR,TNR,SEN,SPEC,Recall,
    ACC,BAC,PPV,NPV,Precision,G,F1,F2,F0.5,Fscore,
    FNR,FPR,FDR,FOR,BER,logloss,LSR,mmce,brier,SSR,
    BM,MK,MCC,Kappa,LRpos,LRneg,AUC)
    )
}

Ta test thử hàm này với 1 block CV và 30 lượt (epoch=30)

# V-folds Cross-validation

set.seed(2405)
cv_splits <- vfold_cv(data=train_df,v = 10,strata ="CLASS")

cvdf=large_cv_func(epoch = 30,
              cv_splits$splits[[1]],
              seed=2405,
              batch_size = 100, 
              verbose = 0)
## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

Hàm chạy tốt,

cvdf
##       TP TN FN FP TPR       TNR SEN      SPEC Recall       ACC       BAC
## [1,] 122  9  0  2   1 0.8181818   1 0.8181818      1 0.9849624 0.9090909
##           PPV NPV Precision         G        F1       F2     F0.5
## [1,] 0.983871   1  0.983871 0.9919027 0.9918699 0.996732 0.987055
##         Fscore FNR       FPR        FDR FOR       BER    logloss
## [1,] 0.9918699   0 0.1818182 0.01612903   0 0.9323308 0.07647281
##              LSR       mmce     brier       SSR        BM       MK
## [1,] -0.07647281 0.01503759 0.9079204 0.9147508 0.8181818 0.983871
##            MCC     Kappa LRpos LRneg AUC
## [1,] 0.8972098 0.8919578   5.5     0   1

Ta bắt đầu chạy thử quy trình kiểm chứng chéo 10 blocks này (sẽ hơi mất thời gian, bạn có thể đi ăn/uống cái gì đó)

epoch_values <-seq(20,120,by=20)

out=tibble(epoch=NA,fold=NA)

out=cbind(out,cvdf)
for(k in c(1:6)){
  for(i in c(1:10)){
    temp_id=tibble(epoch=epoch_values[k],fold=i)
    temp_out=large_cv_func(epoch=epoch_values[k],
                      cv_splits$splits[[i]],
                      seed=2405,
                      batch_size = 100, 
                      verbose = 0)
    temp=cbind(temp_id,temp_out)
    out=rbind(out,temp)
  }
}
## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.

## Warning in cohen.kappa1(x, w = w, n.obs = n.obs, alpha = alpha, levels =
## levels): upper or lower confidence interval exceed abs(1) and set to +/- 1.
mean_out<-out%>%.[-1,]%>%
  group_by(epoch)%>%
  summarize_if(is.numeric,mean)
mean_out%>%gather(TPR,TNR,SEN,SPEC,Recall,AUC,LRpos,LRneg,key="Utility",value="Score")%>%
ggplot(.,aes(x = epoch, y = Score,col=Utility)) + 
  geom_point()+ 
  geom_path()+ 
  theme_bw(8)+
  facet_wrap(~Utility,scales="free")+
  ggtitle("Utility")

mean_out%>%gather(ACC,BAC,PPV,NPV,Precision,G,F1,F2,F0.5,Fscore,
                  key="Accuracy",value="Score")%>%
  ggplot(.,aes(x = epoch, y = Score,col=Accuracy)) + 
  geom_point()+ 
  geom_path()+ 
  theme_bw(8)+
  facet_wrap(~Accuracy,scales="free",ncol=3)+
  ggtitle("Accuracy")

mean_out%>%gather(FNR,FPR,FDR,FOR,BER,logloss,LSR,mmce,brier,SSR,
                  key="Error",value="Score")%>%
  ggplot(.,aes(x = epoch, y = Score,col=Error)) + 
  geom_point()+ 
  geom_path()+ 
  theme_bw(8)+
  facet_wrap(~Error,scales="free",ncol=3)+
  ggtitle("Error")

mean_out%>%gather(BM,MK,MCC,Kappa,
                  key="Agreement",value="Score")%>%
  ggplot(.,aes(x = epoch, y = Score,col=Agreement)) + 
  geom_point()+ 
  geom_path()+ 
  theme_bw(8)+
  facet_wrap(~Agreement,scales="free")+
  ggtitle("Agreement")

Kết quả của quy trình kiểm chứng chéo 10 block cho thấy giá trị epoch tối ưu nằm giữa 80 và 100, khi đó các tiêu chí phẩm chất mô hình bắt đầu ổn định.

Ta huấn luyện lại mô hình lần cuối với epoch=100

model<-build_model()

fit_keras <- fit(
  object=model,
  x=as.matrix(train_df[,-7]), 
  y=train_label,
  batch_size       = 100,
  epochs           = 100,
  verbose=0
)

fit_keras
## Trained on 1,321 samples (batch_size=100, epochs=100)
## Final epoch (plot to see history):
##            loss: 0.01747
## binary_accuracy: 0.9955
plot(fit_keras)+theme_bw()

7 Bước 6: Kiểm định độc lập

Bây giờ ta sẽ kiểm định mô hình cuối cùng trên tập test_df

Pred_class_vector<- 
  predict_classes(object = model, 
                  x = as.matrix(test_df[,-7]))%>%as.vector()

Pred_Prob_vector<- 
  predict_proba(object = model, 
                x = as.matrix(test_df[,-7]))%>%as.vector()

Đầu tiên, một confusion matrix cho thấy quy luật chẩn đoán dựa vào Deep neural network có độ chính xác rất cao:

valid_df <- tibble(
  truth      = as.factor(test_label) %>% 
    fct_recode (Positive = "1", Negative = "0"),
  estimate   = as.factor(Pred_class_vector) %>% 
    fct_recode (Positive = "1", Negative = "0"),
  class_prob = Pred_Prob_vector )

valid_df %>% conf_mat (truth, estimate)
##           Truth
## Prediction Negative Positive
##   Negative       96        8
##   Positive       12     1205

Tính hữu dụng của mô hình được đo bằng 6 tiêu chí, bao gồm AUC= 0.997, cả độ nhạy và độ đặc hiệu đều cao

valid_df%>%mutate(Truth=.$truth,
                  BinTruth=if_else(testset$CLASS=="P",1,0),
                  ClassNeg=1-.$class_prob,
                  ClassPos=.$class_prob,
                  ClassLab=.$estimate)->dfpred

TP=with(dfpred,sum(ClassLab==Truth & ClassLab=="Positive"))
TN=with(dfpred,sum(ClassLab==Truth & ClassLab!="Positive"))
FN=with(dfpred,sum(ClassLab!=Truth & Truth=="Positive"))
FP=with(dfpred,sum(ClassLab!=Truth & Truth!="Positive"))

TPR=TP/(TP+FN)
TNR=TN/(TN+FP)
SEN=TPR
SPEC=TNR
Recall=SEN

myroc <- roc(Truth~ClassPos, data=dfpred)

AUC=myroc$auc

data_frame(fpr=1-myroc$specificities,
           tpr=myroc$sensitivities)%>%
  ggplot(aes(x=fpr,ymin=0,ymax=tpr))+
  geom_polygon(aes(y=tpr),fill="red",alpha=0.3)+
  geom_path(aes(y=tpr),col="red3",size=1)+
  geom_abline(linetype='dashed')+
  theme_bw()+
  coord_equal()+
  labs(x="False positive rate",y="True Positive rate")+
  ggtitle(paste0("AUC=",round(AUC,3)))

rbind(TPR,TNR,SEN,SPEC,Recall)
##             [,1]
## TPR    0.9934048
## TNR    0.8888889
## SEN    0.9934048
## SPEC   0.8888889
## Recall 0.9934048

Tiếp theo, 10 tiêu chí đánh giá độ chính xác của mô hình.2 tiêu chí quan trọng nhất bao gồm BAC =0.99, F1 score rất cao ~ 0.99

ACC=(TP+TN)/(TP+FP+TN+FN)
BAC=(TPR+TNR)/2
PPV=TP/(TP+FP)
NPV=TN/(TN+FN)
Precision=PPV
G=sqrt(Recall*Precision)
F2=(1+2^2)*(Precision*Recall)/((2^2)*Precision+Recall)
F0.5=(1+(0.5)^2)*(Precision*Recall)/((0.5^2)*Precision+Recall)
F1=2*TP/(TP+TP+FP+FN)
Fscore=(1+1)*(Precision*Recall)/(1*Precision+Recall)

rbind(ACC,BAC,PPV,NPV,Precision,G,F1,F2,F0.5,Fscore)
##                [,1]
## ACC       0.9848600
## BAC       0.9411468
## PPV       0.9901397
## NPV       0.9230769
## Precision 0.9901397
## G         0.9917709
## F1        0.9917695
## F2        0.9927500
## F0.5      0.9907910
## Fscore    0.9917695

Nguy cơ sai lầm của mô hình rất thấp: logloss=0.155, brier score=0.01 và BER=0.01

FNR=FN/(FN+TP)
FPR=FP/(FP+TN)
FDR=FP/(TP+FP)
FOR=1-NPV
BER=with(dfpred,mean((ClassPos>0.5 & BinTruth==0) | (ClassPos<0.5 & BinTruth==1)))
logloss=with(dfpred,-mean(log10(ClassPos)))
LSR=with(dfpred,mean(log10(ClassPos)))
mmce=with(dfpred,mean(Truth!=ClassLab))

library(scoring)
brier= dfpred%>%scoring::brierscore(data=.,BinTruth~ClassPos)%>%mean()

SSR=dfpred%>%
  scoring::sphscore(data=.,BinTruth~ClassPos)%>%mean()

rbind(FNR,FPR,FDR,FOR,BER,logloss,LSR,mmce,brier,SSR)
##                 [,1]
## FNR      0.006595218
## FPR      0.111111111
## FDR      0.009860312
## FOR      0.076923077
## BER      0.015140045
## logloss  0.165678366
## LSR     -0.165678366
## mmce     0.015140045
## brier    0.011227514
## SSR      0.012143483

Cuối cùng, mô hình có tính tương hợp cao giữa kết quả phân loại và quan sát thực tế: Kappa=0.908

BM=TPR+TNR-1
MK=PPV+NPV-1
MCC=(TP*TN-FP*FN)/(sqrt((TP+FP)*(TP+FN)*(TN+FP)*(TN+FN)))
## Warning in (TP + FP) * (TP + FN) * (TN + FP) * (TN + FN): NA produit par
## débordement d'entier par le haut
Kappa=psych::cohen.kappa(as.matrix(dfpred[,c(1,2)]))%>%.$weighted.kappa

rbind(BM,MK,MCC,Kappa)
##            [,1]
## BM    0.8822937
## MK    0.9132166
## MCC          NA
## Kappa 0.8974331

8 Bước 7: Giải nghĩa mô hình bằng LIME

Tuy quy tắc chẩn đoán mà ta vừa tạo ra có độ chính xác rất cao, nhưng nó cũng là một dạng mô hình bất khả tri (blackbox). Như ta biết, có một package tên là lime cho phép giải thích cách thức hoạt động của mô hình blackbox, thông qua cơ chế có tên là : Local Interpretable Model-Agnostic Explanations, tạm dịch : « Phép diễn giải cục bộ cho mô hình bất khả tri ».

Phương pháp LIME được giới thiệu lần đầu tiên năm 2016 trong một bài báo của 3 data scientists tại ĐH Washinton là Marco Tulio Ribeiro, Sameer Singh và Carlos Guestrin. https://arxiv.org/abs/1602.04938

Sau khi ứng dụng trong Python năm 2016, một phiên bản ứng dụng cho R do Thomas Lin Pedersen và Michaël Benesty đồng tác giả vừa mới được công bố trên CRAN tháng 9 năm 2017. https://cran.r-project.org/web/packages/lime/index.html https://github.com/thomasp85/lime

Một tutorial ngắn được giới thiệu kèm theo package https://cran.r-project.org/web/packages/lime/vignettes/Understanding_lime.html

Cơ chế hoạt động của LIME

Lime diễn giải bất cứ mô hình classifier nào, bao gồm các mô hình « bất khả tri » hay hộp đen (blackbox) theo quy trình như sau :

Phương pháp LIME dựa trên một giả định cơ bản, đó là bất kể mô hình phức tạp đến đâu, thì tại một miền cục bộ trong không gian dữ liệu (không gian nhỏ xíu xung quanh một điểm), mô hình có thể được ước lượng xấp xỉ bằng quy luật tuyến tính. Bạn có thể hình dung trên một vài bệnh nhân có đặc tính input data xấp xỉ như nhau, thì kết quả output của model sẽ như nhau, đó là bằng chứng về sự tuyến tính cục bộ.

Đầu tiên, LIME sẽ lấy thông tin về đặc tính phân phối của feature (dữ liệu đầu vào) dựa vào training dataset và nội dung (feature nào được dùng) trong model. Thông tin này được lưu trữ trong một object gọi là explainer.

Việc diễn giải sẽ được áp dụng cho 1 trường hợp cá thể, mới (unseen case), thí dụ một bệnh nhân bất kì trích từ tập kiểm định (testing subset).

LIME sẽ mô phỏng một lượng lớn các trường hợp giả định (một đám mây nhiễu của features) nằm kề cận chung quanh trường hợp (điểm) đang được xét, dựa vào quy luật phân phối của features mà nó đã ghi nhận từ trước.

Sau đó LIME áp dụng mô hình cho toàn bộ những điểm trong không gian nhiễu này, đồng thời tính khoảng cách giữa các điểm mô phỏng đến điểm trung tâm là trường hợp được xét. Khoảng cách này sẽ được chuyển thành thang điểm (score).

Tiếp theo, LIME chọn một số lượng M features tiêu biểu nhất cho phép mô tả tốt nhất khoảng cách nói trên.

Cuối cùng, LIME dựng một mô hình rất đơn giản cho các điểm mô phỏng, sử dụng M features được chọn làm predictor, để giải nghĩa cho outcome của model. Mô hình này có dạng Tuyến tính, hoặc mô hình Decision tree. Tham số hồi quy cho mỗi Features được điều chỉnh bằng một trong số (Weight) tỉ lệ với khoảng cách sai biệt với giá trị feature có thực của cá thể.

Việc diễn giải tính hợp lý của kết quả được thực hiện dựa vào Weight coefficient và danh sách M features được chọn. Nếu Weight coefficient > 0, thì giá trị quan sát của feature Mi đang ủng hộ cho kết quả tiên lượng (outcome) P, ngược lại, Weight Coefficient <0 thì giá trị feature Mi chống lại kết quả P.

Thí dụ, ta thử giải thích kết quả của mô hình trên 3 bệnh nhân (nhãn P) và 3 người nhãn N:

library(lime)
## 
## Attaching package: 'lime'
## The following object is masked from 'package:dplyr':
## 
##     explain
model_type.keras.models.Sequential <- function(x, ...) {
  "classification"}

predict_model.keras.models.Sequential <- function (x, newdata, type, ...) {
  pred <- predict_proba (object = x, x = as.matrix(newdata))
  data.frame (Positive = pred, Negative = 1 - pred) }

predict_model(x= model, 
               newdata = test_df[,-7], 
               type    = 'raw')%>%as_tibble()
## # A tibble: 1,321 x 2
##    Positive    Negative
##       <dbl>       <dbl>
##  1   1.000  0.00000238 
##  2   1      0          
##  3   1.000  0.0000186  
##  4   1.000  0.000000119
##  5   1.000  0.000114   
##  6   1.000  0.00000620 
##  7   0.0250 0.975      
##  8   1.000  0.000000834
##  9   1      0          
## 10   1.000  0.00000989 
## # ... with 1,311 more rows
explainer <- lime (
  x              = train_df[,-7], 
  model          = model, 
  bin_continuous = FALSE)

test_pos=test_df%>%dplyr::filter(CLASS=="P")%>%.[1:3,-7]
test_neg=test_df%>%dplyr::filter(CLASS=="N")%>%.[1:3,-7]

testlime=rbind(test_pos,test_neg)

explanation <- lime::explain (
  testlime, # Just to show first 10 cases
  explainer = explainer, 
  n_labels = 1, # explaining a `single class`(Polarity)
  n_features = 5, # returns top four features critical to each case
  kernel_width = 0.5) 

plot_features (explanation)+theme_minimal(5)

Ta cũng có thể huy động hàng loạt trường hợp để tổng kết về vai trò của từng features một cách tổng quát trong mô hình ANN:

test_pos=test_df%>%dplyr::filter(CLASS=="P")%>%.[1:50,-7]
test_neg=test_df%>%dplyr::filter(CLASS=="N")%>%.[1:50,-7]

testlime=rbind(test_pos,test_neg)

explanation2 <- lime::explain (
  testlime,
  explainer = explainer, 
  n_labels = 1, # explaining a `single class`(Polarity)
  n_features = 5, # returns top four features critical to each case
  kernel_width = 0.5) 

plot_explanations(explanation2)+theme_minimal(8)

Kết quả cho thấy, 3 features có vai trò quan trọng nhất lần lượt là TSH, On_Thyroxine treatment và FTI, cho phép phân định nhãn P hoặc N trong đa số trường hợp. Cụ thể, TSH thấp và FTI cao sẽ làm tăng xác suất của nhãn P và ngược lại.

9 Kết luận

Bài thực hành đến đây là hết, Nhi đã chuyển đến các bạn một quy trình tương đối hoàn chỉnh và chuẩn mực cho một nghiên cứu với mục tiêu xây dựng quy luật chẩn đoán có sự hỗ trợ của máy tính.

Như đã bàn ở trên, mô hình Deep neural network giải quyết khá hiệu quả bài toán chẩn đoán bệnh lý theo kiểu nhị phân. Việc huấn luyện mô hình tương đối nhẹ nhàng với điều kiện dữ liệu được chuẩn bị phù hợp và một chút kinh nghiệm về cấu trúc layer và các hàm activation, hàm loss cũng như tiêu chí huấn luyện.

Ta hoàn toàn có thể tinh chỉnh các tham số trong cấu trúc ANN cũng như tham số huấn luyện như bacht size, epoch,… package rsample cho phép tạo ra các quy trình tái chọn mẫu như ý thích, tứ boostrap cho đến kiểm chứng chéo ngẫu nhiên Montecarlo, kiểm chứng chéo K block …

Chúc các bạn thành công.

LS0tDQp0aXRsZTogIkRlZXAgbGVhcm5pbmc6IELDoGkgdG/DoW4gbmjhu4sgcGjDom4iIA0KYXV0aG9yOiAiTMOqIE5n4buNYyBLaOG6oyBOaGkiDQpkYXRlOiAiMjQgVGjDoW5nIDUgMjAxOCINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIGNvZGVfZm9sZGluZzogaGlkZQ0KICAgIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJkZWZhdWx0Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUpDQpgYGANCg0KIVtdKGtlcmFzYmluYXJ5LnBuZykNCg0KIyDEkOG6t3QgduG6pW4gxJHhu4ENCg0KVHJvbmcgbsSDbSAyMDE3LCBz4buxIHJhIMSR4budaSBj4bunYSBwYWNrYWdlIGtlcmFzIMSRw6MgY2hvIHBow6lwIG5nxrDhu51pIGTDuW5nIFIgY8OzIHRo4buDIHRo4buxYyBow6BuaCDEkcaw4bujYyBt4buZdCBwaMawxqFuZyBwaMOhcCBtw7QgaMOsbmggdGh14buZYyBsb+G6oWkgdMOibiB0aeG6v24gbmjhuqV0LCDEkcOzIGzDoCBt4bqhbmcgdGjhuqduIGtpbmggbmjDom4gdOG6oW8gc8OidSAoRGVlcCBuZXVyYWwgbmV0d29yayBoYXkgRGVlcCBsZWFybmluZykgduG7m2kgVGVuc29mRmxvdyAodHLGsOG7m2MgxJHDonkgYuG6oW4gcGjhuqNpIGTDuW5nIFB5dGhvbikuIFBhY2thZ2Uga2VyYXMgY2hvIHBow6lwIFIga+G6v3QgbuG7kWkgduG7m2kgVGVuc29yRmxvdyBiYWNrLWVuZCBj4bunYSBQeXRob24gxJHhu4MgZOG7sW5nIG3DtCBow6xuaCBEZWVwIEFOTiB24bubaSBuaGnhu4F1IOG7qW5nIGThu6VuZyBo4buvdSDDrWNoIG5oxrAgaOG7k2kgcXV5LCB44bq/cCBsb+G6oWkgbmjhu4sgcGjDom4gaGF5IMSRYSBnacOhIHRy4buLLCBjb21wdXRlciB2aXNpb24gKGNvbnZvbHV0aW9uYWwgTmV1cmFsIG5ldHdvcmspLCBk4buxIGLDoW8gY2h14buXaSB0aOG7nWkgZ2lhbiwgbcO0IGjDrG5oIHThu7Egc2luaCBt4bqrdSwgdnYNCg0K4bueIGLDoGkgdHLGsOG7m2MsIE5oaSDEkcOjIHRyw6xuaCBiw6B5IDEg4bupbmcgZOG7pW5nIEjhu5NpIHF1eSDEkWnhu4NuIGjDrG5oIHbhu5tpIGtlcmFzLiBUcm9uZyBiw6BpIG7DoHksIE5oaSBz4bq9IHRp4bq/cCB04bulYyBnaeG7m2kgdGhp4buHdSBt4buZdCBtw7QgaMOsbmggcGjDom4gbG/huqFpIHbhu5tpIGvhur90IHF14bqjIG5o4buLIHBow6JuIChCaW5hcnkgQ2xhc3NpZmljYXRpb24pLlRyb25nIGLDoGksIE5oaSBz4bq9IHRo4buxYyBoaeG7h24gdHXhuqduIHThu7EgdOG7q25nIGLGsOG7m2MgY+G7p2EgbeG7mXQgcXV5IHRyw6xuaCBt4bqrdSBt4buxYyBjaG8gbmdoacOqbiBj4bupdSDhu6luZyBk4bulbmcgTWFjaGluZSBsZWFybmluZy4gS2hpIHRo4buxYyBz4buxIGLhuq90IHRheSB2w6BvIGzDoG0gbmdoacOqbiBj4bupdSBi4bqhbiBjxaluZyBz4bq9IHBo4bqjaSDEkWkgcXVhIG5o4buvbmcgYsaw4bubYyBuw6B5LCBiYW8gZ+G7k206DQoNCisgQmnhu4duIGx14bqtbiBs4buxYSBjaOG7jW4gQWxnb3JpdGhtDQoNCisgVGjEg20gZMOyIGThu68gbGnhu4d1DQoNCisgSG/DoW4gY2h1eeG7g24sIHPGoSBjaOG6vyBk4buvIGxp4buHdQ0KDQorIEh14bqlbiBsdXnhu4duIG3DtCBow6xuaA0KDQorIEtp4buDbSDEkeG7i25oIG3DtCBow6xuaA0KDQorIEJp4buHbiBsdeG6rW4sIGdp4bqjaSBuZ2jEqWEgbcO0IGjDrG5oDQoNCk3hu5l0IG3hu6VjIHRpw6p1IGtow6FjIGPhu6dhIGLDoGkgbsOgeSwgxJHDsyBsw6AgZ2nDunAgY2hvIGPDoWMgYuG6oW4gbMOgbSBxdWVuIHbhu5tpIG3hu5l0IHBob25nIGPDoWNoIHZp4bq/dCBSIGNvZGUgY8WpbmcgbmjGsCBxdXkgdHLDrG5oIHBow6JuIHTDrWNoIGThu68gbGnhu4d1IGhvw6BuIHRvw6BuIG3hu5tpIGzhuqEgYuG6sW5nIGPDoWNoIOG7qW5nIGThu6VuZyBuaOG7r25nIHBhY2thZ2UgbeG7m2kgbmjhuqV0IGPhu6dhIDIgdMOhYyBnaeG6oyBIYWRsZXkgV2lja2hhbSB2w6AgTWF4IEt1aG4sIG5oxrAgcmVjaXBlcyAoc8ahIGNo4bq/IGThu68gbGnhu4d1KSwgcnNhbXBsZSAodMOhaSBjaOG7jW4gbeG6q3UpLCBkcGx5ciAodGhhbyB0w6FjIGThu68gbGnhu4d1IHbDoCB0w7NtIHThuq90KSwgdsOgIGPDoWMgdG/DoW4gdOG7rSBwaXBlIHRyb25nIHRpZHl2ZXJzZS4NCg0KVHJvbmcgYsOgaSBuw6B5LCBOaGkgc+G7rSBk4bulbmcgYuG7mSBz4buRIGxp4buHdSB24buBIGLhu4duaCBTdXkgZ2nDoXAgY+G7p2EgdMOhYyBnaeG6oyBKIFJvc3MgUXVpbmxhbiB2w6Agdmnhu4duIEdhcnZhbiAow5pjKSAoMTk4NykuIEThu68gbGnhu4d1IG7DoHkgZ+G7k20gaMahbiAzNzAwIHRyxrDhu51uZyBo4bujcCB24bubaSBt4bulYyB0acOqdSBuZ2hpw6puIGPhu6l1IChnaeG6oyDEkeG7i25oKSBsw6AgeMOieSBk4buxbmcgbeG7mXQgbcO0IGjDrG5oIGNo4bqpbiDEkW/DoW4gYuG7h25oIFN1eSBHacOhcCAoSHlwb3RoeXJvaWQpIGThu7FhIHbDoG8gdGjDtG5nIHRpbiBn4buTbSA2IGJp4bq/biBz4buRIGxpw6puIHThu6VjIDogVHXhu5VpIHbDoCBnacOhIHRy4buLIGPDoWMgYmlvbWFya2VyIG5oxrAgaG9ybW9uZSBUMywgVFQ0LCBUNFUgdsOgIEZUSSwgdsOgIDEyIGJp4bq/biBuaOG7iyBwaMOibiBn4buTbSBHaeG7m2kgdMOtbmgsIMSRaeG7gXUgdHLhu4sgdGh5cm94aW5lLCBjw7MgdGhhaSwgcGjhuqt1IHRodeG6rXQgdHV54bq/biBnacOhcCwgc3V5IHR1eeG6v24gbsOjbyB0aMO5eSAoaHlwb3BpdHVpdGFyeSApLCB0cmnhu4d1IGNo4bupbmcgYuG7qXUgKGdvaXRyZSx0dW1vciksdMOibSBsw70gKHBzeWNoKSwgxJFp4buBdSB0cuG7iyBJMTMxLCBsaXRoaXVtLiBQaMOibiBsb+G6oWkgYuG7h25oIG5oxrDhu6NjIGdpw6FwIHRyb25nIGThu68gbGnhu4d1IG5ndXnDqm4gdGjhu6d5IGPDsyDEkeG6v24gNSBuaMOjbiBr4bq/dCBxdeG6oyBsw6AgTmVnYXRpdmUsIGh5cG90aHlyb2lkLCBwcmltYXJ5IGh5cG90aHlyb2lkLCBjb21wZW5zYXRlZCBoeXBvdGh5cm9pZCB2w6Agc2Vjb25kYXJ5IGh5cG90aHlyb2lkLiDEkOG7gyDEkcahbiBnaeG6o24gaMOzYSwgdGEgc+G7rSBk4bulbmcgcGhpw6puIGLhuqNuIGdp4bqjbiBsxrDhu6NjIGPhu6dhIFF1YW4gU3VuIHRyw6puIHRoxrAgdmnhu4duIE9wZW5NTCB0cm9uZyDEkcOzIGJp4bq/biBr4bq/dCBxdeG6oyDEkcaw4bujYyBnaeG6o24gbMaw4bujYyBjaOG7iSBjw7JuIDIgbmjDo24gKG5o4buLIHBow6JuKSBsw6AgUG9zaXRpdmUgdsOgIE5lZ2F0aXZlLg0KDQojIELGsOG7m2MgMTogVGjEg20gZMOyIGThu68gbGnhu4d1DQoNCg0KYGBge3J9DQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCg0KZGY9cmVhZC5jc3YoImh0dHBzOi8vd3d3Lm9wZW5tbC5vcmcvZGF0YS9nZXRfY3N2LzUzNTM0L2h5cG90aHlyb2lkLmNzdiIsbmEuc3RyaW5ncyA9ICI/IikNCg0KZGY9ZGYlPiVkcGx5cjo6c2VsZWN0KGFnZSxzZXgscHJlZ25hbnQsDQogICAgICAgICAgICAgICAgICAgICAgb24udGh5cm94aW5lLHF1ZXJ5Lm9uLnRoeXJveGluZSwNCiAgICAgICAgICAgICAgICAgICAgICBvbi5hbnRpdGh5cm9pZC5tZWRpY2F0aW9uLA0KICAgICAgICAgICAgICAgICAgICAgIHRoeXJvaWQuc3VyZ2VyeSwNCiAgICAgICAgICAgICAgICAgICAgICBJMTMxLnRyZWF0bWVudCxzaWNrLA0KICAgICAgICAgICAgICAgICAgICAgIGxpdGhpdW0sZ29pdHJlLHR1bW9yLGh5cG9waXR1aXRhcnkscHN5Y2gsDQogICAgICAgICAgICAgICAgICAgICAgVFNILFQzLFRUNCxUNFUsRlRJLGJpbmFyeUNsYXNzKQ0KDQpkZj1kZiU+JWZpbHRlciguLGFnZTwxMDApJT4lbmEub21pdCgpDQoNCmNvbG5hbWVzKGRmKT1jKCJBR0UiLCJTRVgiLCJQUkVHIiwiT05fVEhZIiwiUU9fVEhZIiwiT05fQVQiLCJTVVJHIiwNCiAgICAgICAgICAgICAgICJJMTMxIiwiU0lDSyIsIkxJVCIsIkdPSVQiLCJUVU1PUiIsIkhQSSIsIlBTWSIsDQogICAgICAgICAgICAgICAiVFNIIiwiVDMiLCJUVDQiLCJUNFUiLCJGVEkiLCJDTEFTUyIpDQoNCnN0cihkZikNCmBgYA0KDQpOaMawIHbhuq15LCBj4bqldSB0csO6YyBk4buvIGxp4buHdSBn4buTbSBjw7M6IDEgYmnhur9uIGvhur90IHF14bqjIG5o4buLIHBow6JuIGzDoCBDTEFTUywgMTMgZmVhdHVyZXMgbMOgIGJp4bq/biDEkeG7i25oIHTDrW5oIG5o4buLIHBow6JuLCA2IGJp4bq/biDEkeG7i25oIGzGsOG7o25nIGxpw6puIHThu6VjLiBE4buvIGxp4buHdSBn4buTbSAyNjQyIHRyxrDhu51uZyBo4bujcA0KDQpgYGB7cn0NCnRhYmxlKGRmJENMQVNTKQ0KYGBgDQoNClThu5VuZyBj4buZbmcgY8OzIDI0MjYgdHLGsOG7nW5nIGjhu6NwIFBvc2l0aXZlIHbDoCAyMTYgdHLGsOG7nW5nIGjhu6NwIE5lZ2F0aXZlLiBOaMawIHbhuq15IGPDsyBz4buxIGLhuqV0IHjhu6luZyB24buBIG5ow6NuIGvhur90IHF14bqjLCBn4bujaSDDvSBy4bqxbmcgdHJvbmcgcXXDoSB0csOsbmggdMOhaSBjaOG7jW4gbeG6q3UgdsOgIHBow6JuIGNoaWEsIHRhIHBo4bqjaSBj4buRIGfhuq9uZyBi4bqjbyB0b8OgbiB04buJIGzhu4cgZ2nhu69hIDIgbmjDo24gUCB2w6AgTi4NCg0KxJBp4buBdSDEkeG6p3UgdGnDqm4gTmhpIHPhur0gbMOgbSwgxJHDsyBsw6AgY+G6r3Qgbmfhuqt1IG5oacOqbiBk4buvIGxp4buHdSBn4buRYyB0aMOgbmggMiBwaOG6p24gYuG6sW5nIG5oYXUgKGRvIHRhIGPDsyBxdcOhIG5oaeG7gXUgZOG7ryBsaeG7h3UsIG7Dqm4gdOG7iSBs4buHIDUwLzUwIGzDoCBsw70gdMaw4bufbmcpLiBDw7MgdGjhu4MgbMOgbSB2aeG7h2MgbsOgeSBjaOG7iSBi4bqxbmcgMyBkw7JuZyBjb2RlIHbhu5tpIGjDoG0gaW5pdGlhbF9zcGxpdCBj4bunYSBnw7NpIHJzYW1wbGUuIA0KDQpW4bubaSB0w7l5IGNo4buJbmggc3RyYXRhIGNobyBiaeG6v24gQ0xBU1MsIHRhIGPDsyB0aOG7gyBi4bqjbyB0b8OgbiB04buJIGzhu4cgZ2nhu69hIDIgbmjDo24gUC9OIG5oxrAgbmhhdSBjaG8gMiB04bqtcCB0cmFpbnNldCB2w6AgdGVzdHNldCBsw6AgMTIxMy8xMDgNCg0KYGBge3J9DQpkYXQ9ZGYNCg0KIyBQcmlvciBTcGxpdGluZyANCg0KbGlicmFyeShyc2FtcGxlKQ0Kc2V0LnNlZWQoMjQwNSkNCg0KZGF0YV9zcGxpdCA8LSBpbml0aWFsX3NwbGl0KGRhdCwgc3RyYXRhID0gIkNMQVNTIixwcm9wPTAuNSkNCg0KdHJhaW5zZXQgPC0gdHJhaW5pbmcoZGF0YV9zcGxpdCkNCnRlc3RzZXQgPC0gdGVzdGluZyhkYXRhX3NwbGl0KQ0KDQp0YWJsZSh0cmFpbnNldCRDTEFTUykNCnRhYmxlKHRlc3RzZXQkQ0xBU1MpDQpgYGANCg0KVGEgc+G6vSBraMO0bmcgbmjDrG4gdsOgbyB04bqtcCB0ZXN0c2V0LCB2w6wgxJHDonkgbMOgIGThu68gbGnhu4d1IGTDuW5nIMSR4buDIGtp4buDbSDEkeG7i25oIG3DtCBow6xuaCwgbmjGsG5nIHRhIGPDsyB0aOG7gyBraOG6o28gc8OhdCB04bqtcCB0cmFpbnNldC4gTmhpIHPhur0gZMO5bmcgcGjGsMahbmcgcGjDoXAgdHLhu7FjIHF1YW4gxJHhu4MgxJHDoW5oIGdpw6EgcGjDom4gYuG7kSBj4bunYSBjw6FjIGJp4bq/biB2w6Agc28gc8OhbmggZ2nhu69hIDIgcGjDom4gbmjDs20gUC9ODQoNCmBgYHtyfQ0KIyBGaXJzdCBFeHBsb3JhdGlvbg0KDQp0cmFpbnNldCU+JWdhdGhlcihBR0UsVFNILFQzLFRUNCxUNFUsRlRJLGtleT0iRmVhdHVyZSIsdmFsdWU9IlNjb3JlIiklPiUNCiAgZ2dwbG90KGFlcyh4PVNjb3JlLGZpbGw9Q0xBU1MpKSsNCiAgZ2VvbV9kZW5zaXR5KGFscGhhPTAuNSxjb2w9ImJsYWNrIikrDQogIHRoZW1lX2J3KCkrDQogIGZhY2V0X3dyYXAofkZlYXR1cmUsc2NhbGVzPSJmcmVlIixuY29sPTMpKw0KICBzY2FsZV9maWxsX21hbnVhbCh2YWx1ZXM9YygiYmx1ZSIsInJlZCIpKQ0KDQp0cmFpbnNldCU+JWdhdGhlcihQUkVHOlBTWSxrZXk9IkZlYXR1cmUiLHZhbHVlPSJTdGF0dXMiKSU+JQ0KICBnZ3Bsb3QoYWVzKHg9U3RhdHVzLGZpbGw9Q0xBU1MpKSsNCiAgZ2VvbV9iYXIoc3RhdD0iY291bnQiLHBvc2l0aW9uPSJmaWxsIixhbHBoYT0wLjUsY29sPSJibGFjayIpKw0KICBzY2FsZV95X2NvbnRpbnVvdXMobGFiZWxzPU5VTEwpKw0KICB0aGVtZV9idygpKw0KICBmYWNldF93cmFwKH5GZWF0dXJlLHNjYWxlcz0iZnJlZSIsbmNvbD02KSsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoImJsdWUiLCJyZWQiKSkNCmBgYA0KDQrhu54gaMOsbmggdGjhu6kgbmjhuqV0LCB0YSBjw7MgdGjhu4Mgbmjhuq1uIMSR4buLbmggcuG6sW5nIGjhuqd1IGjhur90IGJp4bq/biDEkeG7i25oIGzGsOG7o25nIGPDsyBwaMOibiBi4buRIGLhuqV0IHRoxrDhu51uZywga2jDtG5nIMSR4buTbmcgZOG6oW5nIHbDoCBraMO0bmcgY8OibiDEkeG7kWkuIMSQaeG7gXUgbsOgeSBn4bujaSDDvSBy4bqxbmcgbeG7mXQgcGjDqXAgaG/DoW4gY2h1eeG7g24gxJHhu4MgaGnhu4d1IGNo4buJbmggU2tld25lc3MgKHRow60gZOG7pSBCb3gtQ294KSBj4bqnbiDEkcaw4bujYyDDoXAgZOG7pW5nLiBOZ2/DoGkgcmEsIGRvIHRhIHPhur0gZMO5bmcgQU5OLCB2w6AgZ2nhuqNpIHRodeG6rXQgbsOgeSBjaOG7iSBob+G6oXQgxJHhu5luZyB04buRaSDGsHUga2hpIGThu68gbGnhu4d1IMSR4bqndSB2w6BvIMSRxrDhu6NjIGNodeG6qW4gaMOzYSB24buBIGPDuW5nIHRoYW5nIMSRbywgbsOqbiB0YSBz4bq9IMOhcCBk4bulbmcgQ2VudGVyaW5nIHbDoCBTY2FsZSBjaG8gdG/DoG4gYuG7mSBiaeG6v24gxJHhu4tuaCBsxrDhu6NuZy4NCg0KSMOsbmggdGjhu6kgaGFpIGNobyB0aOG6pXkgc+G7sSB0xrDGoW5nIHBo4bqjbiBj4bunYSBjw6FjIGJp4bq/biDEkeG7i25oIHTDrW5oIHRyb25nIGThu68gbGnhu4d1IGzDoCBy4bqldCB54bq/dSwgdGjhuq1tIGNow60gZ+G7o2kgw70gcuG6sW5nIHRhIHBo4bqjaSBsb+G6oWkgYuG7jyBt4buZdCBz4buRIGJp4bq/biBraMO0bmcgY2hvIHRo4bqleSBz4buxIHTGsMahbmcgcGjhuqNuIHLDtSBuaMawIEkxMzEsIFNJQ0suDQoNCiMgQsaw4bubYyAyOiBDaHXhuqluIGLhu4sgZOG7ryBsaeG7h3UNCg0KQsaw4bubYyB0aeG6v3AgdGhlbyBz4bq9IGzDoCBob8OhbiBjaHV54buDbiBk4buvIGxp4buHdSwgY8WpbmcgbmjGsCBxdcOhIHRyw6xuaCBjaHXhuqluIGLhu4sgbmd1ecOqbiBsaeG7h3UgcGjDuSBo4bujcCB24bubaSBtw7NuIMSDbiBtw6AgdGEgbXXhu5FuIG7huqV1LiDEkOG7gyBsw6BtIHZp4buHYyBuw6B5LCB0YSBz4bq9IGTDuW5nIHBhY2thZ2UgcmVjaXBlcyDEkeG7gyB04bqhbyByYSAxIGPDtG5nIHRo4bupYyBu4bqldSDEg24gY8OzIG7hu5lpIGR1bmcgbmjGsCBzYXU6DQoNCjEpIMSQ4bqndSB0acOqbiwgbOG6pXkgdG/DoG4gYuG7mSBmZWF0dXJlcyAsDQoNCjIpIFNhdSDEkcOzLCBsw6BtIGhvw6FuIGNodXnhu4NuIEJveC1Db3ggY2hvIHRvw6BuIGLhu5kgbmjhu69uZyBiaeG6v24gxJHhu4tuaCBsxrDhu6NuZw0KDQozKSBUaeG6v3AgdGhlbywgY2h14bqpbiBow7NhIHRvw6BuIGLhu5kgYmnhur9uIMSR4buLbmggbMaw4bujbmcNCg0KNCkgU2F1IMSRw7MgbG/huqFpIGLhu48gbmjhu69uZyBiaeG6v24gxJHhu4tuaCBsxrDhu6NuZyB0xrDGoW5nIHF1YW4gbeG6oW5oIHbhu5tpIG5oYXUsIHbDoCBjaOG7iSBnaeG7ryBs4bqhaSBuaOG7r25nIGJp4bq/biBraMO0bmcgY8OzIHTGsMahbmcgcXVhbg0KDQo1KSBUaeG6v3AgdGhlbywgY2h1eeG7g24gbmjhu69uZyBiaeG6v24gxJHhu4tuaCB0w61uaCB0aMOgbmggZHVtbXkgdmFyaWFibGUgY8OzIGdpw6EgdHLhu4sgMC8xDQoNCjYpIEN14buRaSBjw7luZywgbG/huqFpIGLhu48gdOG6pXQgY+G6oyBuaOG7r25nIGJp4bq/biBuaOG7iyBwaMOibiBs4bqrbiDEkeG7i25oIGzGsOG7o25nIG7DoG8gY8OzIHZhcmlhbmNlIHLhuqV0IHRo4bqlcCAoPTAgaGF5IGfhuqduID0wKS4NCg0KUXV5IHRyw6xuaCBsacOqbiBob8OgbiB0csOqbiDEkcOieSBjw7MgduG6uyBwaOG7qWMgdOG6oXAsIG5oxrBuZyBraGkgZMO5bmcgcmVjaXBlcyBt4buNaSBjaHV54buHbiBo4bq/dCBz4bupYyBk4buFIGTDoG5nLCBi4bqhbiBjaOG7iSB2aeG7h2MgxJHhurd0IGLGsOG7m2MgbsOgeSBzYXUgYsaw4bubYyBraWEgdsOgIHjDonUgY2h14buXaSBi4bqxbmcgcGlwZXMsIHThuq1uIGPDuW5nIGLhurFuZyBow6BtIHByZXAuDQoNCktoaSDDoXAgZOG7pW5nIGPDtG5nIHRo4bupYyBuw6B5IHRyw6puIHRyYWluc2V0LCB0YSBz4bq9IHRodSDEkcaw4bujYyB04bqtcCBk4buvIGxp4buHdSBkw7luZyDEkeG7gyBk4buxbmcgbcO0IGjDrG5oLA0KDQpUxrDGoW5nIHThu7EsIGtoaSDDoXAgZOG7pW5nIGNobyB0ZXN0c2V0LCB0YSBz4bq9IHRodSDEkcaw4bujYyBk4buvIGxp4buHdSBkw7luZyDEkeG7gyBraeG7g20gxJHhu4tuaCBtw7QgaMOsbmgNCg0KYGBge3J9DQojIHJlY2lwZXMNCg0KbGlicmFyeShyZWNpcGVzKQ0KDQpyZWNfb2JqPC1yZWNpcGUoQ0xBU1N+LiwgZGF0YSA9IHRyYWluc2V0KSU+JQ0KICBzdGVwX0JveENveChhbGxfbnVtZXJpYygpLCAtYWxsX291dGNvbWVzKCkpJT4lDQogIHN0ZXBfY2VudGVyKGFsbF9udW1lcmljKCksLWFsbF9vdXRjb21lcygpKSU+JQ0KICBzdGVwX3NjYWxlKGFsbF9udW1lcmljKCksLWFsbF9vdXRjb21lcygpKSU+JQ0KICBzdGVwX2NvcnIoYWxsX251bWVyaWMoKSwtYWxsX291dGNvbWVzKCkpJT4lDQogIHN0ZXBfZHVtbXkoYWxsX25vbWluYWwoKSwtYWxsX291dGNvbWVzKCkpJT4lDQogIHN0ZXBfenYoYWxsX3ByZWRpY3RvcnMoKSwtYWxsX291dGNvbWVzKCkpJT4lDQogIHN0ZXBfbnp2KGFsbF9wcmVkaWN0b3JzKCksLWFsbF9vdXRjb21lcygpKSU+JQ0KICBwcmVwKHRyYWluaW5nID0gdHJhaW5zZXQpDQoNCnRyYWluX2RmIDwtIGJha2UocmVjX29iaiwgbmV3ZGF0YSA9IHRyYWluc2V0KQ0KdGVzdF9kZiA8LSBiYWtlKHJlY19vYmosIG5ld2RhdGEgPSB0ZXN0c2V0KQ0KDQpzdHIodHJhaW5fZGYpDQpgYGANCg0KTmjGsCB24bqteSwgc2F1IGtoaSBzxqEgY2jhur8gdGEgxJHDoyBsb+G6oWkgYuG7jyDEkcaw4bujYyBo4bqndSBo4bq/dCBiaeG6v24gxJHhu4tuaCB0w61uaCBjw7MgcGjGsMahbmcgc2FpIHF1w6EgdGjhuqVwIHbDoCBjaHXhuqluIGjDs2EgaG/DoG4gdG/DoG4gYmnhur9uIMSR4buLbmggbMaw4bujbmcNCg0KYGBge3J9DQp0cmFpbl9kZiU+JQ0KICBnYXRoZXIoQUdFOkZUSSxrZXk9IkZlYXR1cmUiLHZhbHVlPSJTY29yZSIpJT4lDQogIGdncGxvdChhZXMoeD1TY29yZSxmaWxsPUNMQVNTKSkrDQogIGdlb21fZGVuc2l0eShhbHBoYT0wLjUsY29sPSJibGFjayIpKw0KICB0aGVtZV9idygpKw0KICBmYWNldF93cmFwKH5GZWF0dXJlLHNjYWxlcz0iZnJlZSIsbmNvbD0zKSsNCiAgc2NhbGVfZmlsbF9tYW51YWwodmFsdWVzPWMoImJsdWUiLCJyZWQiKSkNCg0KdHJhaW5fZGYlPiUNCiAgZ2F0aGVyKFNFWF9NOlBTWV90LGtleT0iRmVhdHVyZSIsdmFsdWU9IlN0YXR1cyIpJT4lDQogIGdncGxvdChhZXMoeD1mYWN0b3IoU3RhdHVzKSxmaWxsPUNMQVNTKSkrDQogIGdlb21fYmFyKHN0YXQ9ImNvdW50Iixwb3NpdGlvbj0iZmlsbCIsYWxwaGE9MC41LGNvbD0iYmxhY2siKSsNCiAgc2NhbGVfeV9jb250aW51b3VzKGxhYmVscz1OVUxMKSsNCiAgdGhlbWVfYncoKSsNCiAgZmFjZXRfd3JhcCh+RmVhdHVyZSxzY2FsZXM9ImZyZWUiLG5jb2w9NikrDQogIHNjYWxlX2ZpbGxfbWFudWFsKHZhbHVlcz1jKCJibHVlIiwicmVkIikpDQpgYGANCg0KVGEgaHkgduG7jW5nIHbhu5tpIGThu68gbGnhu4d1IG7DoHksIG3DtCBow6xuaCBz4bq9IGhv4bqhdCDEkeG7mW5nIHThu5FpIMawdSA6DQoNCiMgQsaw4bubYyAzOiBYw6J5IGThu7FuZyBj4bqldSB0csO6YyBt4bqhbmcgbmV1cm9uDQoNClRyxrDhu5tjIGtoaSBs4buxYSBjaOG7jW4gbeG7mXQgZ2nhuqNpIHRodeG6rXQgKGFsZ29yaXRobSkgTWFjaGluZSBsZWFybmluZyBjaG8gbmdoacOqbiBj4bupdSBj4bunYSBtw6xuaCwgdGEgY+G6p24gYmnhu4duIGx14bqtbiB24buBIGPGoSBz4bufIGPhu6dhIGzhu7FhIGNo4buNbiBuw6B5LiBUcm9uZyB0aMOtIGThu6UgbsOgeSwgTmhpIHBo4bqjaSBnaeG6o2kgdGjDrWNoIHThuqFpIHNhbyBs4bqhaSBkw7luZyBt4buZdCBtw7QgaMOsbmggYmxhY2tib3ggdGhheSB2w6wgaOG7k2kgcXV5IGxvZ2lzdGljID8NCg0KTcO0IGjDrG5oIE5ldXJhbCBuZXR3b3JrIHBow7kgaOG7o3AgdHJvbmcgMiB0csaw4budbmcgaOG7o3AgOg0KDQorIE3hu5l0IGLhu4duaCBsw70gbcOgIHRyaeG7h3UgY2jhu6luZyBsw6JtIHPDoG5nLCBy4buRaSBsb+G6oW4gc2luaCBsw70sIHNpbmggaMOzYSDEkcOjIMSRxrDhu6NjIG3DtCB04bqjIG3hu5l0IGPDoWNoIGtpbmggxJFp4buDbiwgxJHhuqd5IMSR4bunIHRyb25nIHkgdsSDbiB2w6Aga2jDtG5nIGPDsm4gZ8OsIHBo4bqjaSBiw6BuIGPDo2kgbuG7r2EsIHR1eSBuaGnDqm4gbeG7pWMgdGnDqnUgY+G7p2EgbmdoacOqbiBj4bupdSBraMO0bmcgcGjhuqNpIGzDoCBkaeG7hW4gZOG7i2NoL3Bow6JuIHTDrWNoLCBuaMawbmcgbmjhurFtIHRoaeG6v3QgbOG6rXAgbeG7mXQgY8O0bmcgY+G7pSB04buxIMSR4buZbmcgKHNvZnR3YXJlKSBo4buXIHRy4bujIGLDoWMgc8SpIMSRxrBhIHJhIHF1eeG6v3QgxJHhu4tuaCBjaOG6qW4gxJFvw6FuIHRyb25nIG5o4buvbmcgdHLGsOG7nW5nIGjhu6NwIGtow7MuDQoNCisgROG7ryBsaeG7h3UgxJHhuqd1IHbDoG8gY8OzIGLhuqNuIGNo4bqldCBwaOG7qWMgdOG6oXAsIHbGsOG7o3QgcmEga2jhu49pIHF1eSB04bqvYyB0aMO0bmcgdGjGsOG7nW5nIGPhu6dhIG3DtG4gVGjhu5FuZyBrw6osIHRow60gZOG7pSBow6xuaCDhuqNuaCAo4bqjbmggY2jhu6VwIG7hu5lpIHNvaSwgZ2nhuqNpIHBo4bqrdSBi4buHbmgsIE1SSSwg4oCmKSwgw6JtIHRoYW5oICh0aeG6v25nIHRpbSwgw6JtIGjDtCBo4bqlcCksIHTDrW4gaGnhu4d1IMSRaeG7h24gKEVDRywgRUVHKSwgdsSDbiBi4bqjbiAoY2jhu68gdmnhur90IHRheSwgYuG7h25oIMOhbiDEkWnhu4duIHThu60p4oCmLCB2w6wgRGVlcCBsZWFybmluZyBjaG8gcGjDqXAgcGjDom4gdMOtY2ggcuG6pXQgdOG7kXQgbmjhu69uZyBsb+G6oWkgZOG7ryBsaeG7h3UgbsOgeS4NCg0KVGjDrSBk4bulIGhp4buHbiB0aOG7nWkgdGh14buZYyB0csaw4budbmcgaOG7o3AgdGjhu6kgbmjhuqV0OiBN4bulYyB0acOqdSBnaeG6oyDEkeG7i25oIGzDoCB0aGnhur90IGzhuq1wIG3hu5l0IHF1eSBsdeG6rXQgY2jhuqluIMSRb8OhbiB04buxIMSR4buZbmcsIGPDoG5nIGNow61uaCB4w6FjIGPDoG5nIHThu5F0IG5oxrBuZyBraMO0bmcgcXVhbiB0w6JtIMSR4bq/biBsacOqbiBo4buHIGdp4buvYSBjw6FjIGJp4bq/biB0cm9uZyBk4buvIGxp4buHdS4NCg0KVHLGsOG7m2MgaOG6v3QsIHRhIHRyw61jaCB4deG6pXQgMiB2ZWN0b3JzIGPhu6dhIGJp4bq/biBDTEFTUyBzYXUga2hpIGNodXnhu4NuIG7DsyB0aMOgbmggYmnhur9uIG5o4buLIHBow6JuICgwLzEsIGNobyB04bqtcCB0cmFpbl9kZiB2w6AgdGVzdF9kZikNCg0KYGBge3J9DQp0cmFpbl9sYWJlbCA8LSBpZmVsc2UocHVsbCh0cmFpbl9kZiwgQ0xBU1MpID09ICJQIiwgMSwgMCkNCg0KdGVzdF9sYWJlbCA8LSBpZmVsc2UocHVsbCh0ZXN0X2RmLCBDTEFTUykgPT0gIlAiLCAxLCAwKQ0KYGBgDQoNClRp4bq/cCB0aGVvLCB0YSBk4buxbmcgY+G6pXUgdHLDumMgKGFyY2hpdGVjdHVyZSkgY+G7p2EgbeG6oW5nIG5ldXJvbjoNCg0KIVtdKGJpbmFyeWtlcmFzMi5wbmcpDQoNCsSQw6J5IGzDoCBt4buZdCBt4bqhbmcgQU5OIGfhu5NtIDMgbOG7m3AsIDEgbOG7m3AgxJHhuqd1IHbDoG8gZ+G7k20gOSBwZXJjZXB0cm9uIMSR4buDIHRp4bq/cCBuaOG6rW4gOSBmZWF0dXJlcy4gVMOtbiBoaeG7h3UgdHLhu41uZyBz4buRIHPhur0gxJHGsOG7o2Mga8OtY2ggaG/huqF0IGLhurFuZyBow6BtIHJlbHUgLCBzYXUgxJHDsyDEkWkgcXVhIDMgbOG7m3Ag4bqpbiwgZ+G7k20gMSBs4bubcCDhuqluIHRo4bupIG5o4bqldCBjw7MgNjQgbmV1cm9uLGxpw6puIGvhur90IGLDo28gaG/DoG4gduG7m2kgxJHhuqd1IHbDoG8sIHNhdSDEkcOzIHTDrW4gaGnhu4d1IMSRxrDhu6NjIGzhu41jIGzhuqduIHRo4bupIG5o4bqldCBi4bqxbmcgMSBs4bubcCBkcm9wX291dCB24bubaSB04buJIGzhu4cgMC4wMSwgdGnhur9wIHThu6VjIMSRaSBxdWEgbOG7m3Ag4bqpbiB0aOG7qSAyIGPDsyAzMiBuZXVyb24sIHLhu5NpICAxIGzhu5twIGRyb3BvdXQgdGjhu6kgaGFpIHRyxrDhu5tjIGtoaSBr4bq/dCB0aMO6YyDhu58gbOG7m3AgY3Xhu5FpIGPDuW5nIGNo4buJIGPDsyAxIG5ldXJvbiDEkeG7gyB4deG6pXQga+G6v3QgcXXhuqMsIGvDrWNoIGhv4bqhdCBi4bqxbmcgaMOgbSBzaWdtb2lkLg0KDQpN4bqhbmcgQU5OIG7DoHkgc2F1IMSRw7MgxJHGsOG7o2MgY29tcGlsZSB24bubaSB0acOqdSBjaMOtIGh14bqlbiBsdXnhu4duIGzDoCBiaW5hcnlfYWNjdXJhY3ksIGjDoG0gbG9zcyBsw6AgYmluYXJ5X2Nyb3NzZW50cm9weSB2w6Agb3B0aW1pemVyIGzDoCBow6BtICJhZGFtIiANCg0KIyBCxrDhu5tjIDU6IEh14bqlbiBsdXnhu4duIG3DtCBow6xuaA0KDQpUYSBjaOG6oXkgdGjhu60gY+G6pXUgdHLDumMgbsOgeSB24bubaSA1MCBsxrDhu6N0IGzhuqV5IG3huqt1IHbhu5tpIGvDrWNoIHRoxrDhu5tjIDEwMCB0csaw4budbmcgaOG7o3AgdsOgIGtp4buDbSDEkeG7i25oIHRyw6puIDMwJSANCg0KYGBge3J9DQojIEFOTg0KDQpsaWJyYXJ5KGtlcmFzKQ0KDQpidWlsZF9tb2RlbCA8LSBmdW5jdGlvbigpew0KICBtb2RlbF9rZXJhcyA8LSBrZXJhc19tb2RlbF9zZXF1ZW50aWFsKCklPiUgDQogICAgIyAxc3QgaGlkZGVuIGxheWVyDQogICAgbGF5ZXJfZGVuc2UoDQogICAgICB1bml0cyAgICAgICAgICAgICAgPSA2NCwgDQogICAgICBrZXJuZWxfaW5pdGlhbGl6ZXIgPSAidW5pZm9ybSIsIA0KICAgICAgYWN0aXZhdGlvbiAgICAgICAgID0gInJlbHUiLCANCiAgICAgIGlucHV0X3NoYXBlICAgICAgICA9IG5jb2wodHJhaW5fZGYpLTEpICU+JSANCiAgICAjIDFzdCBEcm9wb3V0IA0KICAgIGxheWVyX2Ryb3BvdXQocmF0ZSA9IDAuMDEpICU+JQ0KICAgICMgMm5kIGhpZGRlbiBsYXllcg0KICAgIGxheWVyX2RlbnNlKA0KICAgICAgdW5pdHMgICAgICAgICAgICAgID0gMzIsIA0KICAgICAga2VybmVsX2luaXRpYWxpemVyID0gInVuaWZvcm0iLCANCiAgICAgIGFjdGl2YXRpb24gICAgICAgICA9ICJyZWx1IikgJT4lIA0KICAgICMgRHJvcG91dCANCiAgICBsYXllcl9kcm9wb3V0KHJhdGUgPSAwLjAxKSAlPiUNCiAgICAjICBPdXRwdXQgbGF5ZXINCiAgICBsYXllcl9kZW5zZSgNCiAgICAgIHVuaXRzICAgICAgICAgICAgICA9IDEsIA0KICAgICAga2VybmVsX2luaXRpYWxpemVyID0gInVuaWZvcm0iLCANCiAgICAgIGFjdGl2YXRpb24gICAgICAgICA9ICJzaWdtb2lkIikgJT4lIA0KICAgICMgQ29tcGlsZSBBTk4NCiAgICBjb21waWxlKA0KICAgICAgb3B0aW1pemVyID0gJ2FkYW0nLA0KICAgICAgbG9zcyAgICAgID0gJ2JpbmFyeV9jcm9zc2VudHJvcHknLA0KICAgICAgbWV0cmljcyAgID0gYygnYmluYXJ5X2FjY3VyYWN5JykNCiAgICApDQp9DQoNCm1vZGVsPC1idWlsZF9tb2RlbCgpDQoNCmZpdF9rZXJhcyA8LSBmaXQoDQogIG9iamVjdD1tb2RlbCwNCiAgeD1hcy5tYXRyaXgodHJhaW5fZGZbLC03XSksIA0KICB5PXRyYWluX2xhYmVsLA0KICBiYXRjaF9zaXplICAgICAgID0gMTAwLA0KICBlcG9jaHMgICAgICAgICAgID0gNTAsIA0KICB2YWxpZGF0aW9uX3NwbGl0ID0gMC4zLA0KICB2ZXJib3NlID0gMA0KKQ0KDQpmaXRfa2VyYXMNCmBgYA0KDQpL4bq/dCBxdeG6oyBodeG6pW4gbHV54buHbiBy4bqldCBraOG6oyBxdWFuLCBhY2N1cmFjeSDEkcOjIMSRxrDhu6NjIHThu5FpIMawdSDEkeG6v24gOTguNTklIHbDoCBsb3NzIGjhuqEgeHXhu5FuZyBjw7JuIDAuMDQNCg0KYGBge3J9DQpwbG90KGZpdF9rZXJhcykrdGhlbWVfYncoKQ0KYGBgDQoNCiMgQsaw4bubYyA0OiBLaeG7g20gY2jhu6luZyBjaMOpbw0KDQpUdXkgbmhpw6puLCBwaOG6qW0gY2jhuqV0IGPhu6dhIG3DtCBow6xuaCBjw7JuIHTDuXkgdGh14buZYyB2w6BvIG5oaeG7gXUgdGhhbSBz4buRIHRyb25nIHF1w6EgdHLDrG5oIGh14bqlbiBsdXnhu4duLCB0aMOtIGThu6UgxJHGoW4gZ2nhuqNuIG5o4bqldCDhu58gxJHDonkgbMOgIHPhu5EgbMaw4bujdCAoZXBvY2gpLCBOaGkgbXXhu5FuIGJp4bq/dCBsaeG7h3UgdMSDbmcgZXBvY2ggY2FvIGjGoW4gY8OzIGzDoG0gdGhheSDEkeG7lWkgaGnhu4d1IHF14bqjIGPhu6dhIG3DtCBow6xuaCBBTk4gaGF5IGtow7RuZyA/DQoNCsSQ4buDIGPDsyBjw6J1IHRy4bqjIGzhu51pLCBOaGkgc+G6vSB0aOG7sWMgaGnhu4duIG3hu5l0IHF1eSB0csOsbmgga2nhu4NtIGNo4bupbmcgY2jDqW8gMTAgYmxvY2tzLCBt4buXaSA5IGJsb2NrIG5oxrAgduG6rXlz4bq9IGTDuW5nIMSR4buDIGh14bqlbiBsdXnhu4duLCBOaGkgc+G6vSB0aOG7rSBs4bqnbiBsxrDhu6N0IGdpw6EgdHLhu4sgZXBvY2ggdMSDbmcgZOG6p24gdOG7qyAyMCw0MCw2MCw4MCwxMDAsMTIwIHbDoCBraeG7g20gxJHhu4tuaCBoaeG7h3UgbsSDbmcgbcO0IGjDrG5oIHRyw6puIDEgYmxvY2sgY8OybiBs4bqhaQ0KDQpOaGkgdmnhur90IDEgaMOgbSB0xrDGoW5nIMSR4buRaSAia2jhu6duZyIgduG7m2kgbuG7mWkgZHVuZyBuaMawIHNhdToNCg0KMSkgS2hhaSBiw6FvIGdpw6EgdHLhu4sgZXBvY2gsIGRhbmggc8OhY2ggdMOhaSBjaOG7jW4gbeG6q3Ugc3BsaXQsIHbDoCBzZWVkIG51bWJlci4NCg0KMikgU2F1IG3hu5dpIGzhuqduIGNo4bqheSwgeMOzYSBi4buZIG5o4bubIGPhu6dhIGtlcmFzIMSR4buDIGNodeG6qW4gYuG7iyBjaG8gbOG6p24gY2jhuqF5IHRp4bq/cCB0aGVvDQoNCjMpIEThu7FuZyAxIG3huqFuZyBuZXVyb24gY8OzIGPhuqV1IHRyw7pjIG5oxrAgdHLDqm4gDQoNCjQpIFThu6sgZGFuaCBzw6FjaCBjaOG7jW4gbeG6q3UsIHRyw61jaCB4deG6pXQgMiBtYXRyaWNlcyB04bqtcCB0cmFpbiwgdGVzdCB2w6AgdmVjdG9yIGxhYmVsIA0KDQo1KSBIdeG6pW4gbHV54buHbiBBTk4gc+G7rSBk4bulbmcgOSBibG9ja3MNCg0KNikgUGjDom4gbG/huqFpIDEgYmxvY2sgKHRlc3QpIGTGsOG7m2kgZOG6oW5nIHByb2JhYmlsaXR5ICgwOjEpIHbDoCBkw6FuIG5ow6NuIFAvTg0KDQo3KSBUw61uaCB04bqldCBj4bqjICh4aW4gbmjhuqVuIG3huqFuaDogdOG6pXQgY+G6oykgdGnDqnUgY2jDrSBwaOG6qW0gY2jhuqV0IGPhu6dhIG1vZGVsIHbhu6thIGThu7FuZywgYmFvIGfhu5NtIDQgbmjDs206IHTDrW5oIGjhu691IGThu6VuZywgdMOtbmggY2jDrW5oIHjDoWMsIG5ndXkgY8ahIHNhaSBs4bqnbSwgc+G7sSB0xrDGoW5nIGjhu6NwDQoNCihYaW4geGVtIGLDoGkgbsOgeSDEkeG7gyBoaeG7g3UgcsO1IGjGoW46IDxodHRwOi8vcnB1YnMuY29tL2xlbmdvY2toYW5oaS8zNDc5NDE+ICkNCg0KOCkgWHXhuqV0IGvhur90IHF14bqjIGtp4buDbSDEkeG7i25oDQoNCmBgYHtyfQ0KbGlicmFyeSh5YXJkc3RpY2spDQpsaWJyYXJ5KHNjb3JpbmcpDQpsaWJyYXJ5KHBST0MpDQpsaWJyYXJ5KHJzYW1wbGUpDQoNCiMgQSBsYXJnZSBDcm9zcyB2YWxpZGF0aW9uIGZ1bmN0aW9uDQoNCg0KbGFyZ2VfY3ZfZnVuYzwtIGZ1bmN0aW9uKGVwb2NoLCBzcGxpdCwgc2VlZCwgLi4uKSB7DQogICMgU2V0IHRoZSBzZWVkIHRvIGdldCByZXByb2R1Y2libGUgc3RhcnRpbmcgdmFsdWVzIGFuZCBkcm9wb3V0cw0KICBzZXQuc2VlZChzZWVkKQ0KICAjIGNsZWFycyBtZW1vcnkgdXNlZCBieSB0aGUgbGFzdCB0cmlhbCAgDQogIG9uLmV4aXQoa2VyYXM6OmJhY2tlbmQoKSRjbGVhcl9zZXNzaW9uKCkpDQoNCiAgIyBEZWZpbmUgQU5ODQogIA0KICBtb2RlbDwtIGtlcmFzX21vZGVsX3NlcXVlbnRpYWwoKSU+JSANCiAgICAjIDFzdCBoaWRkZW4gbGF5ZXINCiAgICBsYXllcl9kZW5zZSgNCiAgICAgIHVuaXRzICAgICAgICAgICAgICA9IDY0LCANCiAgICAgIGtlcm5lbF9pbml0aWFsaXplciA9ICJ1bmlmb3JtIiwgDQogICAgICBhY3RpdmF0aW9uICAgICAgICAgPSAicmVsdSIsIA0KICAgICAgaW5wdXRfc2hhcGUgICAgICAgID0gbmNvbCh0cmFpbl9kZiktMSkgJT4lIA0KICAgICMgMXN0IERyb3BvdXQgDQogICAgbGF5ZXJfZHJvcG91dChyYXRlID0gMC4wMSkgJT4lDQogICAgIyAybmQgaGlkZGVuIGxheWVyDQogICAgbGF5ZXJfZGVuc2UoDQogICAgICB1bml0cyAgICAgICAgICAgICAgPSAzMiwgDQogICAgICBrZXJuZWxfaW5pdGlhbGl6ZXIgPSAidW5pZm9ybSIsIA0KICAgICAgYWN0aXZhdGlvbiAgICAgICAgID0gInJlbHUiKSAlPiUgDQogICAgIyBEcm9wb3V0IA0KICAgIGxheWVyX2Ryb3BvdXQocmF0ZSA9IDAuMDEpICU+JQ0KICAgICMgIE91dHB1dCBsYXllcg0KICAgIGxheWVyX2RlbnNlKA0KICAgICAgdW5pdHMgICAgICAgICAgICAgID0gMSwgDQogICAgICBrZXJuZWxfaW5pdGlhbGl6ZXIgPSAidW5pZm9ybSIsIA0KICAgICAgYWN0aXZhdGlvbiAgICAgICAgID0gInNpZ21vaWQiKSAlPiUgDQogICAgIyBDb21waWxlIEFOTg0KICAgIGNvbXBpbGUoDQogICAgICBvcHRpbWl6ZXIgPSAnYWRhbScsDQogICAgICBsb3NzICAgICAgPSAnYmluYXJ5X2Nyb3NzZW50cm9weScsDQogICAgICBtZXRyaWNzICAgPSBjKCdiaW5hcnlfYWNjdXJhY3knKQ0KICAgICkgIA0KICAjIGRhdGEgdXNlZCBmb3IgdHJhaW5pbmcgKCJhbmFseXNpcyIgc2V0KQ0KICBibG9ja19kZjwtIGFuYWx5c2lzKHNwbGl0KQ0KDQogIGJsb2NrX2xhYjwtaWZlbHNlKHB1bGwoYmxvY2tfZGYsQ0xBU1MpPT0iUCIsMSwwKQ0KICANCiAgbW9kZWwlPiVmaXQoDQogICAgeD1hcy5tYXRyaXgoYmxvY2tfZGZbLC03XSksDQogICAgeT1ibG9ja19sYWIsDQogICAgZXBvY2hzID0gZXBvY2gsIA0KICAgIC4uLg0KICApDQogIA0KICAjIE5vdyBvYnRhaW4gdGhlIGhvbGRvdXQgc2V0IGZvciBwcmVkaWN0aW9uDQogIGJsb2NrdGVzdF9kZjwtIGFzc2Vzc21lbnQoc3BsaXQpDQogIA0KICBibG9ja3Rlc3RfbGFiPC1pZmVsc2UocHVsbChibG9ja3Rlc3RfZGYsQ0xBU1MpPT0iUCIsMSwwKQ0KICANCiAgDQogIFByZWRfY2xhc3NfdmVjdG9yPC0gDQogICAgcHJlZGljdF9jbGFzc2VzKG9iamVjdCA9IG1vZGVsLCANCiAgICAgICAgICAgICAgICAgICAgeCA9IGFzLm1hdHJpeChibG9ja3Rlc3RfZGZbLC03XSkpJT4lYXMudmVjdG9yKCkNCiAgDQogIFByZWRfUHJvYl92ZWN0b3I8LSANCiAgICBwcmVkaWN0X3Byb2JhKG9iamVjdCA9IG1vZGVsLCANCiAgICAgICAgICAgICAgICAgIHggPSBhcy5tYXRyaXgoYmxvY2t0ZXN0X2RmWywtN10pKSU+JWFzLnZlY3RvcigpDQogIA0KICB2YWxpZF9kZiA8LSB0aWJibGUoDQogICAgdHJ1dGggICAgICA9IGFzLmZhY3RvcihibG9ja3Rlc3RfbGFiKSAlPiUgDQogICAgICBmY3RfcmVjb2RlIChQb3NpdGl2ZSA9ICIxIiwgTmVnYXRpdmUgPSAiMCIpLA0KICAgIGVzdGltYXRlICAgPSBhcy5mYWN0b3IoUHJlZF9jbGFzc192ZWN0b3IpICU+JSANCiAgICAgIGZjdF9yZWNvZGUgKFBvc2l0aXZlID0gIjEiLCBOZWdhdGl2ZSA9ICIwIiksDQogICAgY2xhc3NfcHJvYiA9IFByZWRfUHJvYl92ZWN0b3IgKQ0KICANCiAgZGZwcmVkPC12YWxpZF9kZiU+JW11dGF0ZShUcnV0aD0uJHRydXRoLA0KICAgICAgICAgICAgICAgICAgICBCaW5UcnV0aD1pZl9lbHNlKC4kdHJ1dGg9PSJQIiwxLDApLA0KICAgICAgICAgICAgICAgICAgICBDbGFzc05lZz0xLS4kY2xhc3NfcHJvYiwNCiAgICAgICAgICAgICAgICAgICAgQ2xhc3NQb3M9LiRjbGFzc19wcm9iLA0KICAgICAgICAgICAgICAgICAgICBDbGFzc0xhYj0uJGVzdGltYXRlKQ0KICANCiAgVFA9d2l0aChkZnByZWQsc3VtKENsYXNzTGFiPT1UcnV0aCAmIENsYXNzTGFiPT0iUG9zaXRpdmUiKSkNCiAgVE49d2l0aChkZnByZWQsc3VtKENsYXNzTGFiPT1UcnV0aCAmIENsYXNzTGFiIT0iUG9zaXRpdmUiKSkNCiAgRk49d2l0aChkZnByZWQsc3VtKENsYXNzTGFiIT1UcnV0aCAmIFRydXRoPT0iUG9zaXRpdmUiKSkNCiAgRlA9d2l0aChkZnByZWQsc3VtKENsYXNzTGFiIT1UcnV0aCAmIFRydXRoIT0iUG9zaXRpdmUiKSkNCiAgDQogIFRQUj1UUC8oVFArRk4pDQogIFROUj1UTi8oVE4rRlApDQogIFNFTj1UUFINCiAgU1BFQz1UTlINCiAgUmVjYWxsPVNFTg0KICANCiAgQUNDPShUUCtUTikvKFRQK0ZQK1ROK0ZOKQ0KICBCQUM9KFRQUitUTlIpLzINCiAgUFBWPVRQLyhUUCtGUCkNCiAgTlBWPVROLyhUTitGTikNCiAgUHJlY2lzaW9uPVBQVg0KICBHPXNxcnQoUmVjYWxsKlByZWNpc2lvbikNCiAgRjI9KDErMl4yKSooUHJlY2lzaW9uKlJlY2FsbCkvKCgyXjIpKlByZWNpc2lvbitSZWNhbGwpDQogIEYwLjU9KDErKDAuNSleMikqKFByZWNpc2lvbipSZWNhbGwpLygoMC41XjIpKlByZWNpc2lvbitSZWNhbGwpDQogIEYxPTIqVFAvKFRQK1RQK0ZQK0ZOKQ0KICBGc2NvcmU9KDErMSkqKFByZWNpc2lvbipSZWNhbGwpLygxKlByZWNpc2lvbitSZWNhbGwpDQogIA0KICBGTlI9Rk4vKEZOK1RQKQ0KICBGUFI9RlAvKEZQK1ROKQ0KICBGRFI9RlAvKFRQK0ZQKQ0KICBGT1I9MS1OUFYNCiAgQkVSPXdpdGgoZGZwcmVkLG1lYW4oKENsYXNzUG9zPjAuNSAmIEJpblRydXRoPT0wKSB8IChDbGFzc1BvczwwLjUgJiBCaW5UcnV0aD09MSkpKQ0KICBsb2dsb3NzPXdpdGgoZGZwcmVkLC1tZWFuKGxvZzEwKENsYXNzUG9zKSkpDQogIExTUj13aXRoKGRmcHJlZCxtZWFuKGxvZzEwKENsYXNzUG9zKSkpDQogIG1tY2U9d2l0aChkZnByZWQsbWVhbihUcnV0aCE9Q2xhc3NMYWIpKQ0KICANCiAgYnJpZXI9IGRmcHJlZCU+JXNjb3Jpbmc6OmJyaWVyc2NvcmUoZGF0YT0uLEJpblRydXRofkNsYXNzUG9zKSU+JW1lYW4oKQ0KICANCiAgU1NSPWRmcHJlZCU+JQ0KICAgIHNjb3Jpbmc6OnNwaHNjb3JlKGRhdGE9LixCaW5UcnV0aH5DbGFzc1BvcyklPiVtZWFuKCkNCiAgDQoNCiAgQk09VFBSK1ROUi0xDQogIE1LPVBQVitOUFYtMQ0KICBNQ0M9KFRQKlROLUZQKkZOKS8oc3FydCgoVFArRlApKihUUCtGTikqKFROK0ZQKSooVE4rRk4pKSkNCiAgS2FwcGE9cHN5Y2g6OmNvaGVuLmthcHBhKGFzLm1hdHJpeChkZnByZWRbLGMoMSwyKV0pKSU+JS4kd2VpZ2h0ZWQua2FwcGENCiAgDQogIHJiaW5kKEJNLE1LLE1DQyxLYXBwYSkNCiAgDQogIExScG9zPVNFTi8oMS1TUEVDKQ0KICBMUm5lZz0oMS1TRU4pL1NQRUMNCiAgDQogIA0KICBteXJvYyA8LSByb2MoVHJ1dGh+Q2xhc3NQb3MsIGRhdGE9ZGZwcmVkKQ0KICANCiAgQVVDPW15cm9jJGF1Y1sxXQ0KICANCiAgcmV0dXJuKGNiaW5kKA0KICAgIFRQLFROLEZOLEZQLA0KICAgIFRQUixUTlIsU0VOLFNQRUMsUmVjYWxsLA0KICAgIEFDQyxCQUMsUFBWLE5QVixQcmVjaXNpb24sRyxGMSxGMixGMC41LEZzY29yZSwNCiAgICBGTlIsRlBSLEZEUixGT1IsQkVSLGxvZ2xvc3MsTFNSLG1tY2UsYnJpZXIsU1NSLA0KICAgIEJNLE1LLE1DQyxLYXBwYSxMUnBvcyxMUm5lZyxBVUMpDQogICAgKQ0KfQ0KDQpgYGANCg0KVGEgdGVzdCB0aOG7rSBow6BtIG7DoHkgduG7m2kgMSBibG9jayBDViB2w6AgMzAgbMaw4bujdCAoZXBvY2g9MzApDQoNCmBgYHtyfQ0KIyBWLWZvbGRzIENyb3NzLXZhbGlkYXRpb24NCg0Kc2V0LnNlZWQoMjQwNSkNCmN2X3NwbGl0cyA8LSB2Zm9sZF9jdihkYXRhPXRyYWluX2RmLHYgPSAxMCxzdHJhdGEgPSJDTEFTUyIpDQoNCmN2ZGY9bGFyZ2VfY3ZfZnVuYyhlcG9jaCA9IDMwLA0KICAgICAgICAgICAgICBjdl9zcGxpdHMkc3BsaXRzW1sxXV0sDQogICAgICAgICAgICAgIHNlZWQ9MjQwNSwNCiAgICAgICAgICAgICAgYmF0Y2hfc2l6ZSA9IDEwMCwgDQogICAgICAgICAgICAgIHZlcmJvc2UgPSAwKQ0KYGBgDQoNCkjDoG0gY2jhuqF5IHThu5F0LCANCg0KYGBge3J9DQpjdmRmDQpgYGANCg0KVGEgYuG6r3QgxJHhuqd1IGNo4bqheSB0aOG7rSBxdXkgdHLDrG5oIGtp4buDbSBjaOG7qW5nIGNow6lvIDEwIGJsb2NrcyBuw6B5IChz4bq9IGjGoWkgbeG6pXQgdGjhu51pIGdpYW4sIGLhuqFuIGPDsyB0aOG7gyDEkWkgxINuL3Xhu5FuZyBjw6FpIGfDrCDEkcOzKQ0KDQpgYGB7cn0NCmVwb2NoX3ZhbHVlcyA8LXNlcSgyMCwxMjAsYnk9MjApDQoNCm91dD10aWJibGUoZXBvY2g9TkEsZm9sZD1OQSkNCg0Kb3V0PWNiaW5kKG91dCxjdmRmKQ0KYGBgDQoNCg0KYGBge3J9DQoNCmZvcihrIGluIGMoMTo2KSl7DQogIGZvcihpIGluIGMoMToxMCkpew0KICAgIHRlbXBfaWQ9dGliYmxlKGVwb2NoPWVwb2NoX3ZhbHVlc1trXSxmb2xkPWkpDQogICAgdGVtcF9vdXQ9bGFyZ2VfY3ZfZnVuYyhlcG9jaD1lcG9jaF92YWx1ZXNba10sDQogICAgICAgICAgICAgICAgICAgICAgY3Zfc3BsaXRzJHNwbGl0c1tbaV1dLA0KICAgICAgICAgICAgICAgICAgICAgIHNlZWQ9MjQwNSwNCiAgICAgICAgICAgICAgICAgICAgICBiYXRjaF9zaXplID0gMTAwLCANCiAgICAgICAgICAgICAgICAgICAgICB2ZXJib3NlID0gMCkNCiAgICB0ZW1wPWNiaW5kKHRlbXBfaWQsdGVtcF9vdXQpDQogICAgb3V0PXJiaW5kKG91dCx0ZW1wKQ0KICB9DQp9DQpgYGANCg0KYGBge3J9DQptZWFuX291dDwtb3V0JT4lLlstMSxdJT4lDQogIGdyb3VwX2J5KGVwb2NoKSU+JQ0KICBzdW1tYXJpemVfaWYoaXMubnVtZXJpYyxtZWFuKQ0KYGBgDQoNCg0KYGBge3J9DQptZWFuX291dCU+JWdhdGhlcihUUFIsVE5SLFNFTixTUEVDLFJlY2FsbCxBVUMsTFJwb3MsTFJuZWcsa2V5PSJVdGlsaXR5Iix2YWx1ZT0iU2NvcmUiKSU+JQ0KZ2dwbG90KC4sYWVzKHggPSBlcG9jaCwgeSA9IFNjb3JlLGNvbD1VdGlsaXR5KSkgKyANCiAgZ2VvbV9wb2ludCgpKyANCiAgZ2VvbV9wYXRoKCkrIA0KICB0aGVtZV9idyg4KSsNCiAgZmFjZXRfd3JhcCh+VXRpbGl0eSxzY2FsZXM9ImZyZWUiKSsNCiAgZ2d0aXRsZSgiVXRpbGl0eSIpDQoNCm1lYW5fb3V0JT4lZ2F0aGVyKEFDQyxCQUMsUFBWLE5QVixQcmVjaXNpb24sRyxGMSxGMixGMC41LEZzY29yZSwNCiAgICAgICAgICAgICAgICAgIGtleT0iQWNjdXJhY3kiLHZhbHVlPSJTY29yZSIpJT4lDQogIGdncGxvdCguLGFlcyh4ID0gZXBvY2gsIHkgPSBTY29yZSxjb2w9QWNjdXJhY3kpKSArIA0KICBnZW9tX3BvaW50KCkrIA0KICBnZW9tX3BhdGgoKSsgDQogIHRoZW1lX2J3KDgpKw0KICBmYWNldF93cmFwKH5BY2N1cmFjeSxzY2FsZXM9ImZyZWUiLG5jb2w9MykrDQogIGdndGl0bGUoIkFjY3VyYWN5IikNCg0KbWVhbl9vdXQlPiVnYXRoZXIoRk5SLEZQUixGRFIsRk9SLEJFUixsb2dsb3NzLExTUixtbWNlLGJyaWVyLFNTUiwNCiAgICAgICAgICAgICAgICAgIGtleT0iRXJyb3IiLHZhbHVlPSJTY29yZSIpJT4lDQogIGdncGxvdCguLGFlcyh4ID0gZXBvY2gsIHkgPSBTY29yZSxjb2w9RXJyb3IpKSArIA0KICBnZW9tX3BvaW50KCkrIA0KICBnZW9tX3BhdGgoKSsgDQogIHRoZW1lX2J3KDgpKw0KICBmYWNldF93cmFwKH5FcnJvcixzY2FsZXM9ImZyZWUiLG5jb2w9MykrDQogIGdndGl0bGUoIkVycm9yIikNCg0KbWVhbl9vdXQlPiVnYXRoZXIoQk0sTUssTUNDLEthcHBhLA0KICAgICAgICAgICAgICAgICAga2V5PSJBZ3JlZW1lbnQiLHZhbHVlPSJTY29yZSIpJT4lDQogIGdncGxvdCguLGFlcyh4ID0gZXBvY2gsIHkgPSBTY29yZSxjb2w9QWdyZWVtZW50KSkgKyANCiAgZ2VvbV9wb2ludCgpKyANCiAgZ2VvbV9wYXRoKCkrIA0KICB0aGVtZV9idyg4KSsNCiAgZmFjZXRfd3JhcCh+QWdyZWVtZW50LHNjYWxlcz0iZnJlZSIpKw0KICBnZ3RpdGxlKCJBZ3JlZW1lbnQiKQ0KDQpgYGANCg0KS+G6v3QgcXXhuqMgY+G7p2EgcXV5IHRyw6xuaCBraeG7g20gY2jhu6luZyBjaMOpbyAxMCBibG9jayBjaG8gdGjhuqV5IGdpw6EgdHLhu4sgZXBvY2ggdOG7kWkgxrB1IG7hurFtIGdp4buvYSA4MCB2w6AgMTAwLCBraGkgxJHDsyBjw6FjIHRpw6p1IGNow60gcGjhuqltIGNo4bqldCBtw7QgaMOsbmggYuG6r3QgxJHhuqd1IOG7lW4gxJHhu4tuaC4NCg0KVGEgaHXhuqVuIGx1eeG7h24gbOG6oWkgbcO0IGjDrG5oIGzhuqduIGN14buRaSB24bubaSBlcG9jaD0xMDANCg0KYGBge3J9DQptb2RlbDwtYnVpbGRfbW9kZWwoKQ0KDQpmaXRfa2VyYXMgPC0gZml0KA0KICBvYmplY3Q9bW9kZWwsDQogIHg9YXMubWF0cml4KHRyYWluX2RmWywtN10pLCANCiAgeT10cmFpbl9sYWJlbCwNCiAgYmF0Y2hfc2l6ZSAgICAgICA9IDEwMCwNCiAgZXBvY2hzICAgICAgICAgICA9IDEwMCwNCiAgdmVyYm9zZT0wDQopDQoNCmZpdF9rZXJhcw0KDQpwbG90KGZpdF9rZXJhcykrdGhlbWVfYncoKQ0KYGBgDQoNCiMgQsaw4bubYyA2OiBLaeG7g20gxJHhu4tuaCDEkeG7mWMgbOG6rXANCg0KQsOieSBnaeG7nSB0YSBz4bq9IGtp4buDbSDEkeG7i25oIG3DtCBow6xuaCBjdeG7kWkgY8O5bmcgdHLDqm4gdOG6rXAgdGVzdF9kZg0KDQpgYGB7cn0NClByZWRfY2xhc3NfdmVjdG9yPC0gDQogIHByZWRpY3RfY2xhc3NlcyhvYmplY3QgPSBtb2RlbCwgDQogICAgICAgICAgICAgICAgICB4ID0gYXMubWF0cml4KHRlc3RfZGZbLC03XSkpJT4lYXMudmVjdG9yKCkNCg0KUHJlZF9Qcm9iX3ZlY3RvcjwtIA0KICBwcmVkaWN0X3Byb2JhKG9iamVjdCA9IG1vZGVsLCANCiAgICAgICAgICAgICAgICB4ID0gYXMubWF0cml4KHRlc3RfZGZbLC03XSkpJT4lYXMudmVjdG9yKCkNCmBgYA0KDQoNCsSQ4bqndSB0acOqbiwgbeG7mXQgY29uZnVzaW9uIG1hdHJpeCBjaG8gdGjhuqV5IHF1eSBsdeG6rXQgY2jhuqluIMSRb8OhbiBk4buxYSB2w6BvIERlZXAgbmV1cmFsIG5ldHdvcmsgY8OzIMSR4buZIGNow61uaCB4w6FjIHLhuqV0IGNhbzoNCg0KYGBge3J9DQp2YWxpZF9kZiA8LSB0aWJibGUoDQogIHRydXRoICAgICAgPSBhcy5mYWN0b3IodGVzdF9sYWJlbCkgJT4lIA0KICAgIGZjdF9yZWNvZGUgKFBvc2l0aXZlID0gIjEiLCBOZWdhdGl2ZSA9ICIwIiksDQogIGVzdGltYXRlICAgPSBhcy5mYWN0b3IoUHJlZF9jbGFzc192ZWN0b3IpICU+JSANCiAgICBmY3RfcmVjb2RlIChQb3NpdGl2ZSA9ICIxIiwgTmVnYXRpdmUgPSAiMCIpLA0KICBjbGFzc19wcm9iID0gUHJlZF9Qcm9iX3ZlY3RvciApDQoNCnZhbGlkX2RmICU+JSBjb25mX21hdCAodHJ1dGgsIGVzdGltYXRlKQ0KYGBgDQoNClTDrW5oIGjhu691IGThu6VuZyBj4bunYSBtw7QgaMOsbmggxJHGsOG7o2MgxJFvIGLhurFuZyA2IHRpw6p1IGNow60sIGJhbyBn4buTbSBBVUM9IDAuOTk3LCBj4bqjIMSR4buZIG5o4bqheSB2w6AgxJHhu5kgxJHhurdjIGhp4buHdSDEkeG7gXUgY2FvDQoNCmBgYHtyfQ0KdmFsaWRfZGYlPiVtdXRhdGUoVHJ1dGg9LiR0cnV0aCwNCiAgICAgICAgICAgICAgICAgIEJpblRydXRoPWlmX2Vsc2UodGVzdHNldCRDTEFTUz09IlAiLDEsMCksDQogICAgICAgICAgICAgICAgICBDbGFzc05lZz0xLS4kY2xhc3NfcHJvYiwNCiAgICAgICAgICAgICAgICAgIENsYXNzUG9zPS4kY2xhc3NfcHJvYiwNCiAgICAgICAgICAgICAgICAgIENsYXNzTGFiPS4kZXN0aW1hdGUpLT5kZnByZWQNCg0KVFA9d2l0aChkZnByZWQsc3VtKENsYXNzTGFiPT1UcnV0aCAmIENsYXNzTGFiPT0iUG9zaXRpdmUiKSkNClROPXdpdGgoZGZwcmVkLHN1bShDbGFzc0xhYj09VHJ1dGggJiBDbGFzc0xhYiE9IlBvc2l0aXZlIikpDQpGTj13aXRoKGRmcHJlZCxzdW0oQ2xhc3NMYWIhPVRydXRoICYgVHJ1dGg9PSJQb3NpdGl2ZSIpKQ0KRlA9d2l0aChkZnByZWQsc3VtKENsYXNzTGFiIT1UcnV0aCAmIFRydXRoIT0iUG9zaXRpdmUiKSkNCg0KVFBSPVRQLyhUUCtGTikNClROUj1UTi8oVE4rRlApDQpTRU49VFBSDQpTUEVDPVROUg0KUmVjYWxsPVNFTg0KDQpteXJvYyA8LSByb2MoVHJ1dGh+Q2xhc3NQb3MsIGRhdGE9ZGZwcmVkKQ0KDQpBVUM9bXlyb2MkYXVjDQoNCmRhdGFfZnJhbWUoZnByPTEtbXlyb2Mkc3BlY2lmaWNpdGllcywNCiAgICAgICAgICAgdHByPW15cm9jJHNlbnNpdGl2aXRpZXMpJT4lDQogIGdncGxvdChhZXMoeD1mcHIseW1pbj0wLHltYXg9dHByKSkrDQogIGdlb21fcG9seWdvbihhZXMoeT10cHIpLGZpbGw9InJlZCIsYWxwaGE9MC4zKSsNCiAgZ2VvbV9wYXRoKGFlcyh5PXRwciksY29sPSJyZWQzIixzaXplPTEpKw0KICBnZW9tX2FibGluZShsaW5ldHlwZT0nZGFzaGVkJykrDQogIHRoZW1lX2J3KCkrDQogIGNvb3JkX2VxdWFsKCkrDQogIGxhYnMoeD0iRmFsc2UgcG9zaXRpdmUgcmF0ZSIseT0iVHJ1ZSBQb3NpdGl2ZSByYXRlIikrDQogIGdndGl0bGUocGFzdGUwKCJBVUM9Iixyb3VuZChBVUMsMykpKQ0KDQpyYmluZChUUFIsVE5SLFNFTixTUEVDLFJlY2FsbCkNCmBgYA0KDQpUaeG6v3AgdGhlbywgMTAgdGnDqnUgY2jDrSDEkcOhbmggZ2nDoSDEkeG7mSBjaMOtbmggeMOhYyBj4bunYSBtw7QgaMOsbmguMiB0acOqdSBjaMOtIHF1YW4gdHLhu41uZyBuaOG6pXQgYmFvIGfhu5NtIEJBQyA9MC45OSwgRjEgc2NvcmUgIHLhuqV0IGNhbyB+IDAuOTkNCg0KYGBge3J9DQpBQ0M9KFRQK1ROKS8oVFArRlArVE4rRk4pDQpCQUM9KFRQUitUTlIpLzINClBQVj1UUC8oVFArRlApDQpOUFY9VE4vKFROK0ZOKQ0KUHJlY2lzaW9uPVBQVg0KRz1zcXJ0KFJlY2FsbCpQcmVjaXNpb24pDQpGMj0oMSsyXjIpKihQcmVjaXNpb24qUmVjYWxsKS8oKDJeMikqUHJlY2lzaW9uK1JlY2FsbCkNCkYwLjU9KDErKDAuNSleMikqKFByZWNpc2lvbipSZWNhbGwpLygoMC41XjIpKlByZWNpc2lvbitSZWNhbGwpDQpGMT0yKlRQLyhUUCtUUCtGUCtGTikNCkZzY29yZT0oMSsxKSooUHJlY2lzaW9uKlJlY2FsbCkvKDEqUHJlY2lzaW9uK1JlY2FsbCkNCg0KcmJpbmQoQUNDLEJBQyxQUFYsTlBWLFByZWNpc2lvbixHLEYxLEYyLEYwLjUsRnNjb3JlKQ0KDQpgYGANCg0KTmd1eSBjxqEgc2FpIGzhuqdtIGPhu6dhIG3DtCBow6xuaCBy4bqldCB0aOG6pXA6IGxvZ2xvc3M9MC4xNTUsIGJyaWVyIHNjb3JlPTAuMDEgdsOgIEJFUj0wLjAxDQoNCmBgYHtyfQ0KRk5SPUZOLyhGTitUUCkNCkZQUj1GUC8oRlArVE4pDQpGRFI9RlAvKFRQK0ZQKQ0KRk9SPTEtTlBWDQpCRVI9d2l0aChkZnByZWQsbWVhbigoQ2xhc3NQb3M+MC41ICYgQmluVHJ1dGg9PTApIHwgKENsYXNzUG9zPDAuNSAmIEJpblRydXRoPT0xKSkpDQpsb2dsb3NzPXdpdGgoZGZwcmVkLC1tZWFuKGxvZzEwKENsYXNzUG9zKSkpDQpMU1I9d2l0aChkZnByZWQsbWVhbihsb2cxMChDbGFzc1BvcykpKQ0KbW1jZT13aXRoKGRmcHJlZCxtZWFuKFRydXRoIT1DbGFzc0xhYikpDQoNCmxpYnJhcnkoc2NvcmluZykNCmJyaWVyPSBkZnByZWQlPiVzY29yaW5nOjpicmllcnNjb3JlKGRhdGE9LixCaW5UcnV0aH5DbGFzc1BvcyklPiVtZWFuKCkNCg0KU1NSPWRmcHJlZCU+JQ0KICBzY29yaW5nOjpzcGhzY29yZShkYXRhPS4sQmluVHJ1dGh+Q2xhc3NQb3MpJT4lbWVhbigpDQoNCnJiaW5kKEZOUixGUFIsRkRSLEZPUixCRVIsbG9nbG9zcyxMU1IsbW1jZSxicmllcixTU1IpDQpgYGANCg0KQ3Xhu5FpIGPDuW5nLCBtw7QgaMOsbmggY8OzIHTDrW5oIHTGsMahbmcgaOG7o3AgY2FvIGdp4buvYSBr4bq/dCBxdeG6oyBwaMOibiBsb+G6oWkgdsOgIHF1YW4gc8OhdCB0aOG7sWMgdOG6vzogS2FwcGE9MC45MDggDQoNCmBgYHtyfQ0KQk09VFBSK1ROUi0xDQpNSz1QUFYrTlBWLTENCk1DQz0oVFAqVE4tRlAqRk4pLyhzcXJ0KChUUCtGUCkqKFRQK0ZOKSooVE4rRlApKihUTitGTikpKQ0KS2FwcGE9cHN5Y2g6OmNvaGVuLmthcHBhKGFzLm1hdHJpeChkZnByZWRbLGMoMSwyKV0pKSU+JS4kd2VpZ2h0ZWQua2FwcGENCg0KcmJpbmQoQk0sTUssTUNDLEthcHBhKQ0KYGBgDQoNCiMgQsaw4bubYyA3OiBHaeG6o2kgbmdoxKlhIG3DtCBow6xuaCBi4bqxbmcgTElNRQ0KDQpUdXkgcXV5IHThuq9jIGNo4bqpbiDEkW/DoW4gbcOgIHRhIHbhu6thIHThuqFvIHJhIGPDsyDEkeG7mSBjaMOtbmggeMOhYyBy4bqldCBjYW8sIG5oxrBuZyBuw7MgY8WpbmcgbMOgIG3hu5l0IGThuqFuZyBtw7QgaMOsbmggYuG6pXQga2jhuqMgdHJpIChibGFja2JveCkuIE5oxrAgdGEgYmnhur90LCBjw7MgbeG7mXQgcGFja2FnZSB0w6puIGzDoCBsaW1lIGNobyBwaMOpcCBnaeG6o2kgdGjDrWNoIGPDoWNoIHRo4bupYyBob+G6oXQgxJHhu5luZyBj4bunYSBtw7QgaMOsbmggYmxhY2tib3gsIHRow7RuZyBxdWEgY8ahIGNo4bq/IGPDsyB0w6puIGzDoCA6IExvY2FsIEludGVycHJldGFibGUgTW9kZWwtQWdub3N0aWMgRXhwbGFuYXRpb25zLCB04bqhbSBk4buLY2ggOiDCqyBQaMOpcCBkaeG7hW4gZ2nhuqNpIGPhu6VjIGLhu5kgY2hvIG3DtCBow6xuaCBi4bqldCBraOG6oyB0cmkgwrsuDQoNClBoxrDGoW5nIHBow6FwIExJTUUgxJHGsOG7o2MgZ2nhu5tpIHRoaeG7h3UgbOG6p24gxJHhuqd1IHRpw6puIG7Eg20gMjAxNiB0cm9uZyBt4buZdCBiw6BpIGLDoW8gY+G7p2EgMyBkYXRhIHNjaWVudGlzdHMgdOG6oWkgxJBIIFdhc2hpbnRvbiBsw6AgTWFyY28gVHVsaW8gUmliZWlybywgU2FtZWVyIFNpbmdoIHbDoCBDYXJsb3MgR3Vlc3RyaW4uIGh0dHBzOi8vYXJ4aXYub3JnL2Ficy8xNjAyLjA0OTM4DQoNClNhdSBraGkg4bupbmcgZOG7pW5nIHRyb25nIFB5dGhvbiBuxINtIDIwMTYsIG3hu5l0IHBoacOqbiBi4bqjbiDhu6luZyBk4bulbmcgY2hvIFIgZG8gVGhvbWFzIExpbiBQZWRlcnNlbiB2w6AgTWljaGHDq2wgQmVuZXN0eSDEkeG7k25nIHTDoWMgZ2nhuqMgduG7q2EgbeG7m2kgxJHGsOG7o2MgY8O0bmcgYuG7kSB0csOqbiBDUkFOIHRow6FuZyA5IG7Eg20gMjAxNy4gaHR0cHM6Ly9jcmFuLnItcHJvamVjdC5vcmcvd2ViL3BhY2thZ2VzL2xpbWUvaW5kZXguaHRtbCBodHRwczovL2dpdGh1Yi5jb20vdGhvbWFzcDg1L2xpbWUNCg0KTeG7mXQgdHV0b3JpYWwgbmfhuq9uIMSRxrDhu6NjIGdp4bubaSB0aGnhu4d1IGvDqG0gdGhlbyBwYWNrYWdlIGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy9saW1lL3ZpZ25ldHRlcy9VbmRlcnN0YW5kaW5nX2xpbWUuaHRtbA0KDQpDxqEgY2jhur8gaG/huqF0IMSR4buZbmcgY+G7p2EgTElNRQ0KDQpMaW1lIGRp4buFbiBnaeG6o2kgYuG6pXQgY+G7qSBtw7QgaMOsbmggY2xhc3NpZmllciBuw6BvLCBiYW8gZ+G7k20gY8OhYyBtw7QgaMOsbmggwqsgYuG6pXQga2jhuqMgdHJpIMK7IGhheSBo4buZcCDEkWVuIChibGFja2JveCkgdGhlbyBxdXkgdHLDrG5oIG5oxrAgc2F1IDoNCg0KUGjGsMahbmcgcGjDoXAgTElNRSBk4buxYSB0csOqbiBt4buZdCBnaeG6oyDEkeG7i25oIGPGoSBi4bqjbiwgxJHDsyBsw6AgYuG6pXQga+G7gyBtw7QgaMOsbmggcGjhu6ljIHThuqFwIMSR4bq/biDEkcOidSwgdGjDrCB04bqhaSBt4buZdCBtaeG7gW4gY+G7pWMgYuG7mSB0cm9uZyBraMO0bmcgZ2lhbiBk4buvIGxp4buHdSAoa2jDtG5nIGdpYW4gbmjhu48geMOtdSB4dW5nIHF1YW5oIG3hu5l0IMSRaeG7g20pLCBtw7QgaMOsbmggY8OzIHRo4buDIMSRxrDhu6NjIMaw4bubYyBsxrDhu6NuZyB44bqlcCB44buJIGLhurFuZyBxdXkgbHXhuq10IHR1eeG6v24gdMOtbmguIELhuqFuIGPDsyB0aOG7gyBow6xuaCBkdW5nIHRyw6puIG3hu5l0IHbDoGkgYuG7h25oIG5ow6JuIGPDsyDEkeG6t2MgdMOtbmggaW5wdXQgZGF0YSB44bqlcCB44buJIG5oxrAgbmhhdSwgdGjDrCBr4bq/dCBxdeG6oyBvdXRwdXQgY+G7p2EgbW9kZWwgc+G6vSBuaMawIG5oYXUsIMSRw7MgbMOgIGLhurFuZyBjaOG7qW5nIHbhu4Egc+G7sSB0dXnhur9uIHTDrW5oIGPhu6VjIGLhu5kuDQoNCsSQ4bqndSB0acOqbiwgTElNRSBz4bq9IGzhuqV5IHRow7RuZyB0aW4gduG7gSDEkeG6t2MgdMOtbmggcGjDom4gcGjhu5FpIGPhu6dhIGZlYXR1cmUgKGThu68gbGnhu4d1IMSR4bqndSB2w6BvKSBk4buxYSB2w6BvIHRyYWluaW5nIGRhdGFzZXQgdsOgIG7hu5lpIGR1bmcgKGZlYXR1cmUgbsOgbyDEkcaw4bujYyBkw7luZykgdHJvbmcgbW9kZWwuIFRow7RuZyB0aW4gbsOgeSDEkcaw4bujYyBsxrB1IHRy4buvIHRyb25nIG3hu5l0IG9iamVjdCBn4buNaSBsw6AgZXhwbGFpbmVyLg0KDQpWaeG7h2MgZGnhu4VuIGdp4bqjaSBz4bq9IMSRxrDhu6NjIMOhcCBk4bulbmcgY2hvIDEgdHLGsOG7nW5nIGjhu6NwIGPDoSB0aOG7gywgbeG7m2kgKHVuc2VlbiBjYXNlKSwgdGjDrSBk4bulIG3hu5l0IGLhu4duaCBuaMOibiBi4bqldCBrw6wgdHLDrWNoIHThu6sgdOG6rXAga2nhu4NtIMSR4buLbmggKHRlc3Rpbmcgc3Vic2V0KS4NCg0KTElNRSBz4bq9IG3DtCBwaOG7j25nIG3hu5l0IGzGsOG7o25nIGzhu5tuIGPDoWMgdHLGsOG7nW5nIGjhu6NwIGdp4bqjIMSR4buLbmggKG3hu5l0IMSRw6FtIG3Donkgbmhp4buFdSBj4bunYSBmZWF0dXJlcykgbuG6sW0ga+G7gSBj4bqtbiBjaHVuZyBxdWFuaCB0csaw4budbmcgaOG7o3AgKMSRaeG7g20pIMSRYW5nIMSRxrDhu6NjIHjDqXQsIGThu7FhIHbDoG8gcXV5IGx14bqtdCBwaMOibiBwaOG7kWkgY+G7p2EgZmVhdHVyZXMgbcOgIG7DsyDEkcOjIGdoaSBuaOG6rW4gdOG7qyB0csaw4bubYy4NCg0KU2F1IMSRw7MgTElNRSDDoXAgZOG7pW5nIG3DtCBow6xuaCBjaG8gdG/DoG4gYuG7mSBuaOG7r25nIMSRaeG7g20gdHJvbmcga2jDtG5nIGdpYW4gbmhp4buFdSBuw6B5LCDEkeG7k25nIHRo4budaSB0w61uaCBraG/huqNuZyBjw6FjaCBnaeG7r2EgY8OhYyDEkWnhu4NtIG3DtCBwaOG7j25nIMSR4bq/biDEkWnhu4NtIHRydW5nIHTDom0gbMOgIHRyxrDhu51uZyBo4bujcCDEkcaw4bujYyB4w6l0LiBLaG/huqNuZyBjw6FjaCBuw6B5IHPhur0gxJHGsOG7o2MgY2h1eeG7g24gdGjDoG5oIHRoYW5nIMSRaeG7g20gKHNjb3JlKS4NCg0KVGnhur9wIHRoZW8sIExJTUUgY2jhu41uIG3hu5l0IHPhu5EgbMaw4bujbmcgTSBmZWF0dXJlcyB0acOqdSBiaeG7g3UgbmjhuqV0IGNobyBwaMOpcCBtw7QgdOG6oyB04buRdCBuaOG6pXQga2hv4bqjbmcgY8OhY2ggbsOzaSB0csOqbi4NCg0KQ3Xhu5FpIGPDuW5nLCBMSU1FIGThu7FuZyBt4buZdCBtw7QgaMOsbmggcuG6pXQgxJHGoW4gZ2nhuqNuIGNobyBjw6FjIMSRaeG7g20gbcO0IHBo4buPbmcsIHPhu60gZOG7pW5nIE0gZmVhdHVyZXMgxJHGsOG7o2MgY2jhu41uIGzDoG0gcHJlZGljdG9yLCDEkeG7gyBnaeG6o2kgbmdoxKlhIGNobyBvdXRjb21lIGPhu6dhIG1vZGVsLiBNw7QgaMOsbmggbsOgeSBjw7MgZOG6oW5nIFR1eeG6v24gdMOtbmgsIGhv4bq3YyBtw7QgaMOsbmggRGVjaXNpb24gdHJlZS4gVGhhbSBz4buRIGjhu5NpIHF1eSBjaG8gbeG7l2kgRmVhdHVyZXMgxJHGsOG7o2MgxJFp4buBdSBjaOG7iW5oIGLhurFuZyBt4buZdCB0cm9uZyBz4buRIChXZWlnaHQpIHThu4kgbOG7hyB24bubaSBraG/huqNuZyBjw6FjaCBzYWkgYmnhu4d0IHbhu5tpIGdpw6EgdHLhu4sgZmVhdHVyZSBjw7MgdGjhu7FjIGPhu6dhIGPDoSB0aOG7gy4NCg0KVmnhu4djIGRp4buFbiBnaeG6o2kgdMOtbmggaOG7o3AgbMO9IGPhu6dhIGvhur90IHF14bqjIMSRxrDhu6NjIHRo4buxYyBoaeG7h24gZOG7sWEgdsOgbyBXZWlnaHQgY29lZmZpY2llbnQgdsOgIGRhbmggc8OhY2ggTSBmZWF0dXJlcyDEkcaw4bujYyBjaOG7jW4uIE7hur91IFdlaWdodCBjb2VmZmljaWVudCA+IDAsIHRow6wgZ2nDoSB0cuG7iyBxdWFuIHPDoXQgY+G7p2EgZmVhdHVyZSBNaSDEkWFuZyDhu6duZyBo4buZIGNobyBr4bq/dCBxdeG6oyB0acOqbiBsxrDhu6NuZyAob3V0Y29tZSkgUCwgbmfGsOG7o2MgbOG6oWksIFdlaWdodCBDb2VmZmljaWVudCA8MCB0aMOsIGdpw6EgdHLhu4sgZmVhdHVyZSBNaSBjaOG7kW5nIGzhuqFpIGvhur90IHF14bqjIFAuDQoNClRow60gZOG7pSwgdGEgdGjhu60gZ2nhuqNpIHRow61jaCBr4bq/dCBxdeG6oyBj4bunYSBtw7QgaMOsbmggdHLDqm4gMyBi4buHbmggbmjDom4gKG5ow6NuIFApIHbDoCAzIG5nxrDhu51pIG5ow6NuIE46DQoNCmBgYHtyfQ0KbGlicmFyeShsaW1lKQ0KDQptb2RlbF90eXBlLmtlcmFzLm1vZGVscy5TZXF1ZW50aWFsIDwtIGZ1bmN0aW9uKHgsIC4uLikgew0KICAiY2xhc3NpZmljYXRpb24ifQ0KDQpwcmVkaWN0X21vZGVsLmtlcmFzLm1vZGVscy5TZXF1ZW50aWFsIDwtIGZ1bmN0aW9uICh4LCBuZXdkYXRhLCB0eXBlLCAuLi4pIHsNCiAgcHJlZCA8LSBwcmVkaWN0X3Byb2JhIChvYmplY3QgPSB4LCB4ID0gYXMubWF0cml4KG5ld2RhdGEpKQ0KICBkYXRhLmZyYW1lIChQb3NpdGl2ZSA9IHByZWQsIE5lZ2F0aXZlID0gMSAtIHByZWQpIH0NCg0KcHJlZGljdF9tb2RlbCh4PSBtb2RlbCwgDQogICAgICAgICAgICAgICBuZXdkYXRhID0gdGVzdF9kZlssLTddLCANCiAgICAgICAgICAgICAgIHR5cGUgICAgPSAncmF3JyklPiVhc190aWJibGUoKQ0KDQpleHBsYWluZXIgPC0gbGltZSAoDQogIHggICAgICAgICAgICAgID0gdHJhaW5fZGZbLC03XSwgDQogIG1vZGVsICAgICAgICAgID0gbW9kZWwsIA0KICBiaW5fY29udGludW91cyA9IEZBTFNFKQ0KDQp0ZXN0X3Bvcz10ZXN0X2RmJT4lZHBseXI6OmZpbHRlcihDTEFTUz09IlAiKSU+JS5bMTozLC03XQ0KdGVzdF9uZWc9dGVzdF9kZiU+JWRwbHlyOjpmaWx0ZXIoQ0xBU1M9PSJOIiklPiUuWzE6MywtN10NCg0KdGVzdGxpbWU9cmJpbmQodGVzdF9wb3MsdGVzdF9uZWcpDQoNCmV4cGxhbmF0aW9uIDwtIGxpbWU6OmV4cGxhaW4gKA0KICB0ZXN0bGltZSwgIyBKdXN0IHRvIHNob3cgZmlyc3QgMTAgY2FzZXMNCiAgZXhwbGFpbmVyID0gZXhwbGFpbmVyLCANCiAgbl9sYWJlbHMgPSAxLCAjIGV4cGxhaW5pbmcgYSBgc2luZ2xlIGNsYXNzYChQb2xhcml0eSkNCiAgbl9mZWF0dXJlcyA9IDUsICMgcmV0dXJucyB0b3AgZm91ciBmZWF0dXJlcyBjcml0aWNhbCB0byBlYWNoIGNhc2UNCiAga2VybmVsX3dpZHRoID0gMC41KSANCg0KcGxvdF9mZWF0dXJlcyAoZXhwbGFuYXRpb24pK3RoZW1lX21pbmltYWwoNSkNCmBgYA0KDQpUYSBjxaluZyBjw7MgdGjhu4MgaHV5IMSR4buZbmcgaMOgbmcgbG/huqF0IHRyxrDhu51uZyBo4bujcCDEkeG7gyB04buVbmcga+G6v3QgduG7gSB2YWkgdHLDsiBj4bunYSB04burbmcgZmVhdHVyZXMgbeG7mXQgY8OhY2ggdOG7lW5nIHF1w6F0IHRyb25nIG3DtCBow6xuaCBBTk46DQoNCmBgYHtyfQ0KDQp0ZXN0X3Bvcz10ZXN0X2RmJT4lZHBseXI6OmZpbHRlcihDTEFTUz09IlAiKSU+JS5bMTo1MCwtN10NCnRlc3RfbmVnPXRlc3RfZGYlPiVkcGx5cjo6ZmlsdGVyKENMQVNTPT0iTiIpJT4lLlsxOjUwLC03XQ0KDQp0ZXN0bGltZT1yYmluZCh0ZXN0X3Bvcyx0ZXN0X25lZykNCg0KZXhwbGFuYXRpb24yIDwtIGxpbWU6OmV4cGxhaW4gKA0KICB0ZXN0bGltZSwNCiAgZXhwbGFpbmVyID0gZXhwbGFpbmVyLCANCiAgbl9sYWJlbHMgPSAxLCAjIGV4cGxhaW5pbmcgYSBgc2luZ2xlIGNsYXNzYChQb2xhcml0eSkNCiAgbl9mZWF0dXJlcyA9IDUsICMgcmV0dXJucyB0b3AgZm91ciBmZWF0dXJlcyBjcml0aWNhbCB0byBlYWNoIGNhc2UNCiAga2VybmVsX3dpZHRoID0gMC41KSANCg0KcGxvdF9leHBsYW5hdGlvbnMoZXhwbGFuYXRpb24yKSt0aGVtZV9taW5pbWFsKDgpDQpgYGANCg0KS+G6v3QgcXXhuqMgY2hvIHRo4bqleSwgMyBmZWF0dXJlcyBjw7MgdmFpIHRyw7IgcXVhbiB0cuG7jW5nIG5o4bqldCBs4bqnbiBsxrDhu6N0IGzDoCBUU0gsIE9uX1RoeXJveGluZSB0cmVhdG1lbnQgdsOgIEZUSSwgY2hvIHBow6lwIHBow6JuIMSR4buLbmggbmjDo24gUCBob+G6t2MgTiB0cm9uZyDEkWEgc+G7kSB0csaw4budbmcgaOG7o3AuIEPhu6UgdGjhu4MsIFRTSCB0aOG6pXAgdsOgIEZUSSBjYW8gc+G6vSBsw6BtIHTEg25nIHjDoWMgc3XhuqV0IGPhu6dhIG5ow6NuIFAgdsOgIG5nxrDhu6NjIGzhuqFpLiANCg0KIyBL4bq/dCBsdeG6rW4NCg0KQsOgaSB0aOG7sWMgaMOgbmggxJHhur9uIMSRw6J5IGzDoCBo4bq/dCwgTmhpIMSRw6MgY2h1eeG7g24gxJHhur9uIGPDoWMgYuG6oW4gbeG7mXQgcXV5IHRyw6xuaCB0xrDGoW5nIMSR4buRaSBob8OgbiBjaOG7iW5oIHbDoCBjaHXhuqluIG3hu7FjIGNobyBt4buZdCBuZ2hpw6puIGPhu6l1IHbhu5tpIG3hu6VjIHRpw6p1IHjDonkgZOG7sW5nIHF1eSBsdeG6rXQgY2jhuqluIMSRb8OhbiBjw7Mgc+G7sSBo4buXIHRy4bujIGPhu6dhIG3DoXkgdMOtbmguIA0KDQpOaMawIMSRw6MgYsOgbiDhu58gdHLDqm4sIG3DtCBow6xuaCBEZWVwIG5ldXJhbCBuZXR3b3JrIGdp4bqjaSBxdXnhur90IGtow6EgaGnhu4d1IHF14bqjIGLDoGkgdG/DoW4gY2jhuqluIMSRb8OhbiBi4buHbmggbMO9IHRoZW8ga2nhu4N1IG5o4buLIHBow6JuLiBWaeG7h2MgaHXhuqVuIGx1eeG7h24gbcO0IGjDrG5oIHTGsMahbmcgxJHhu5FpIG5o4bq5IG5ow6BuZyB24bubaSDEkWnhu4F1IGtp4buHbiBk4buvIGxp4buHdSDEkcaw4bujYyBjaHXhuqluIGLhu4sgcGjDuSBo4bujcCB2w6AgbeG7mXQgY2jDunQga2luaCBuZ2hp4buHbSB24buBIGPhuqV1IHRyw7pjIGxheWVyIHbDoCBjw6FjIGjDoG0gYWN0aXZhdGlvbiwgaMOgbSBsb3NzIGPFqW5nIG5oxrAgdGnDqnUgY2jDrSBodeG6pW4gbHV54buHbi4NCg0KVGEgaG/DoG4gdG/DoG4gY8OzIHRo4buDIHRpbmggY2jhu4luaCBjw6FjIHRoYW0gc+G7kSB0cm9uZyBj4bqldSB0csO6YyBBTk4gY8WpbmcgbmjGsCB0aGFtIHPhu5EgaHXhuqVuIGx1eeG7h24gbmjGsCBiYWNodCBzaXplLCBlcG9jaCwuLi4gcGFja2FnZSByc2FtcGxlIGNobyBwaMOpcCB04bqhbyByYSBjw6FjIHF1eSB0csOsbmggdMOhaSBjaOG7jW4gbeG6q3UgbmjGsCDDvSB0aMOtY2gsIHThu6kgYm9vc3RyYXAgY2hvIMSR4bq/biBraeG7g20gY2jhu6luZyBjaMOpbyBuZ+G6q3Ugbmhpw6puIE1vbnRlY2FybG8sIGtp4buDbSBjaOG7qW5nIGNow6lvIEsgYmxvY2sgLi4uIA0KDQpDaMO6YyBjw6FjIGLhuqFuIHRow6BuaCBjw7RuZy4NCg==