Giới thiệu
Trong bài thực hành ngắn này, Nhi muốn làm một thí nghiệm nhỏ để so sánh khả năng của 2 giải thuật khác nhau khi áp dụng cho cùng một bài toán hồi quy: một bên là Mô hình Deep neural network bằng Tensorflow (giao thức Keras), bên kia là mô hình Lambda-Mu-Sigma (LMS) thuộc phương pháp GAMLSS. Mỗi loại có thể xem như ứng cử viên mạnh nhất từ trường phái Whiteboxes và Blackboxes.
Tình huống đặt ra, đó là một nghiên cứu dịch tễ nhằm ước tính giá trị dung phổi bình thường (TLC) ở nam giới thuộc chủng tộc Caucasian.
Dữ liệu
library(tidyverse)
df=read.csv("https://raw.githubusercontent.com/kinokoberuji/R-Tutorials/master/LMSmodelGAMLSS.csv", sep=";")%>%as_tibble()
dfM=filter(df,Sex=="M")%>%dplyr::select(-Sex)
head(dfM)%>%knitr::kable()
7.017265 |
18 |
177.5 |
7.648100 |
18 |
181.0 |
6.737513 |
18 |
176.0 |
5.949827 |
22 |
171.0 |
7.389043 |
22 |
180.0 |
7.048908 |
23 |
185.0 |
Mô hình LMS (Gamlss)
Trước hết, ta sẽ dùng mô hình LMS từ package gamlss. Lý thuyết về mô hình LMS đã được Nhi giải thích cặn kẽ trong bài thực hành số 7 series Gamlss : http://rpubs.com/lengockhanhi/342110. Một cách ngắn gọn, LMS là một tập hợp 3 mô hình riêng biệt M,S,L cho 3 tham số : Mu (vị trị trung tâm), Sigma (Scale) và Lambda (Skewness), tương ứng với 3 tham số Mu (link function = log), Sigma (link=log) và Nu (link = identity) của quy luật phân phối Box-Cox-Cole-Green. Mỗi mô hình M,L,S là một mô hình GAM (generalized additive model), cho phép tích hợp thêm Smoothing splines (các hàm điều hòa/hiệu chỉnh) và hàm đa thức (Polynomial). Chính những hàm này cho phép đồ thị LMS model uốn lượn mềm mại theo những hình dạng phức tạp nhất có thể và tăng độ chính xác của mô hình.
Đầu tiên, ta sẽ dùng caret để chia dữ liệu thành 2 phần, 20% dành cho kiểm định và 80% để dựng mô hình:
library(caret)
set.seed(123)
idM=createDataPartition(y=dfM$TLC, p=0.8,list=FALSE)
trainM=dfM[idM,]
testM=dfM[-idM,]
Ta dựng mô hình LMS bằng quy trình Stepwise, trong đó ta sẽ xác định tổ hợp tối ưu cho 2 câu hỏi: Có cần mô hình cho Sigma/Nu hay không ? và: Nếu cần, thì nội dung mô hình là gì ?
Trong trường hợp này Nhi muốn chọn lọc mô hình L,M,S tối ưu dựa vào 3 giả định: hàm đa thức bậc 3 cho Height , bậc 2 cho Age, kèm hoặc không kèm Spline cho Age, công thức này sẽ lần lượt test cho Mu và Sigma, Nu được mặc định là hằng số.
library(gamlss)
nC<-detectCores()
m1=gamlss(data=trainM,
TLC~1,
sigma.formula = TLC~1,
nu.formula = TLC~1,
family=BCCG(mu.link="log"),
trace=FALSE,
parallel="multicore",
ncpus = nC)
m2=stepGAICAll.A(m1, scope=list(lower=~1,
upper=~poly(Age,2)+poly(Height,3)+pb(Age)),
sigma.scope = list(lower=~1,
upper=~poly(Age,2)+poly(Height,3)+pb(Age)),
k=log(length(trainM)),
trace=FALSE,
parallel="multicore",
ncpus = nC
)
## ---------------------------------------------------
## Start: AIC= 546.27
## TLC ~ 1
##
## ---------------------------------------------------
## Start: AIC= 437.74
## ~1
##
## ---------------------------------------------------
## Start: AIC= 435.34
## ~1
##
## ---------------------------------------------------
## Start: AIC= 435.34
## ~pb(Age)
##
## ---------------------------------------------------
## Start: AIC= 435.34
## TLC ~ poly(Height, 3) + pb(Age)
##
## ---------------------------------------------------
Kết quả Stepwise như sau:
Mô hình cho Mu có công thức: Mu ~ poly(Height,3)+Age+ MuSpline.
Mô hình cho Sigma chỉ gồm Sigma ~ Age + SigmSplin.
Nu = hằng số.
summary(m2)
## ******************************************************************
## Family: c("BCCG", "Box-Cox-Cole-Green")
##
## Call:
## gamlss(formula = TLC ~ poly(Height, 3) + pb(Age), sigma.formula = ~pb(Age),
## nu.formula = ~1, family = BCCG(mu.link = "log"),
## data = trainM, trace = FALSE, parallel = "multicore", ncpus = nC)
##
##
## Fitting method: RS()
##
## ------------------------------------------------------------------
## Mu link function: log
## Mu Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.9041367 0.0222386 85.623 < 2e-16 ***
## poly(Height, 3)1 1.1785602 0.1076800 10.945 < 2e-16 ***
## poly(Height, 3)2 -0.3753507 0.1063796 -3.528 0.000527 ***
## poly(Height, 3)3 -0.1604175 0.1038782 -1.544 0.124224
## pb(Age) 0.0009752 0.0005334 1.828 0.069093 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## ------------------------------------------------------------------
## Sigma link function: log
## Sigma Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -2.568038 0.155515 -16.51 <2e-16 ***
## pb(Age) 0.006680 0.003356 1.99 0.048 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## ------------------------------------------------------------------
## Nu link function: identity
## Nu Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 0.05722 0.59892 0.096 0.924
##
## ------------------------------------------------------------------
## NOTE: Additive smoothing terms exist in the formulas:
## i) Std. Error for smoothers are for the linear effect only.
## ii) Std. Error for the linear terms maybe are not accurate.
## ------------------------------------------------------------------
## No. of observations in the fit: 195
## Degrees of Freedom for the fit: 9.53894
## Residual Deg. of Freedom: 185.4611
## at cycle: 5
##
## Global Deviance: 424.8601
## AIC: 443.938
## SBC: 475.1589
## ******************************************************************
Mô hình Deep neural network với Keras
Keras là một nền tảng chuyên dụng cho Deep learning. Bản thể của Keras dùng ngôn ngữ Python, nhưng có giao thức để sử dụng trong R (package keras), bạn cần install trước một gói Python thí dụ Anaconda để có thể sử dụng keras trong R.
Giao thức Keras tiếp nhận một cấu trúc dữ liệu đặc biệt gọi là các tensor. Ta có thể hình dung về tensors như khái niệm tổng quát để gọi tên những đối tượng dữ liệu mà ta từng biết trong R theo số chiều thông tin (dimension). Trong thí dụ này, ta có thể gọi vector biến kết quả là là 1D tensor, còn matrix hay dataframe gồm các features được gọi là 2D tensor.
Trong R, tensor được lưu dưới dạng array, do đó trước hết ta cần hoán chuyển những dataframe hiện có thành array. Điểm khác biệt thứ hai đó là Keras tiếp nhận riêng tensors cho tập features và cho biến kết quả nên ta phải cắt 2 phần này riêng lẻ từ tập train và test
library(keras)
train_data<-trainM%>%dplyr::select(-1)%>%as.matrix()%>%as.array.default(dimnames=NULL) # Convert dataframe to array
dimnames(train_data) <- NULL
train_targets<-trainM%>%.$TLC%>%as.array()
test_data<-testM%>%dplyr::select(-1)%>%as.matrix()%>%as.array.default(dimnames=NULL) # Convert dataframe to array
dimnames(train_data) <- NULL
test_targets<-testM%>%.$TLC%>%as.array()
str(train_data)
## num [1:195, 1:2] 18 18 18 22 23 23 24 25 25 26 ...
str(test_data)
## num [1:48, 1:2] 22 27 32 33 34 34 35 47 51 61 ...
## - attr(*, "dimnames")=List of 2
## ..$ : NULL
## ..$ : chr [1:2] "Age" "Height"
str(train_targets)
## num [1:195(1d)] 7.02 7.65 6.74 5.95 7.05 ...
str(test_targets)
## num [1:48(1d)] 7.39 7.78 8.69 8.04 6.75 ...
Một điểm cần lưu ý tiếp theo đó là Neural network chỉ hoạt động tối ưu khi dữ liệu đầu vào được chuẩn hóa bằng hàm scale, sử dụng trung bình của tập train:
mu <- apply(train_data, 2, mean)
sigma <- apply(train_data, 2, sd)
train_data <- scale(train_data, center = mu, scale = sigma)
test_data <- scale(test_data, center = mu, scale = sigma)
head(train_data)%>%knitr::kable()
-1.616520 |
0.0889939 |
-1.616520 |
0.5362570 |
-1.616520 |
-0.1026903 |
-1.365097 |
-0.7416377 |
-1.302241 |
1.0474149 |
-1.302241 |
-1.7000587 |
Trong thí dụ này, do bài toán rất đơn giản nên ta chỉ cần dùng một mạng neuron có cấu trúc 2 lớp hidden, mỗi lớp gồm 64 đơn vị neurons. Đầu ra của mạng lưới là 1 neuron duy nhất (và không kèm activation function như trong bài toán tiên lượng xác suất) – đủ để xuất kết quả là một con số (scalar) và tuyến tính – chính là giá trị cần ước lượng.
Mạng lưới được đóng gói với loss function là mean squared error (sai số bình phương trung bình) vì đây là bài toán hồi quy. Trong quá trình huấn luyện, tiêu chí được sử dụng để cải thiện hiệu năng của mô hình là MSE (sai số tuyệt đối trung bình).
build_model <- function() {
model <- keras_model_sequential() %>%
layer_dense(units = 64, activation = "relu",
input_shape = dim(train_data)[[2]]) %>%
layer_dense(units = 64, activation = "relu") %>%
layer_dense(units = 1)
model %>% compile(
optimizer = "rmsprop",
loss = "mse",
metrics = c("mae")
)
}
Tiếp theo, ta thực hiện một quy trình kiểm chứng chéo 5 blocks lặp lại 100 lần
set.seed(123)
k <- 5
indices <- sample(1:nrow(train_data))
folds <- cut(1:length(indices), breaks = k, labels = FALSE)
num_epochs <- 100
all_mae_histories <- NULL
for (i in 1:k) {
cat("processing fold #", i, "\n")
# Prepare the validation data: data from partition # k
val_indices <- which(folds == i, arr.ind = TRUE)
val_data <- train_data[val_indices,]
val_targets <- train_targets[val_indices]
# Prepare the training data: data from all other partitions
partial_train_data <- train_data[-val_indices,]
partial_train_targets <- train_targets[-val_indices]
# Build the Keras model (already compiled)
model <- build_model()
# Train the model (in silent mode, verbose=0)
history <- model %>% fit(
partial_train_data, partial_train_targets,
validation_data = list(val_data, val_targets),
epochs = num_epochs, batch_size = 1, verbose = 0
)
mae_history <- history$metrics$val_mean_absolute_error
all_mae_histories <- rbind(all_mae_histories, mae_history)
}
## processing fold # 1
## processing fold # 2
## processing fold # 3
## processing fold # 4
## processing fold # 5
median_mae_history <- data.frame(
epoch = seq(1:ncol(all_mae_histories)),
validation_mae = apply(all_mae_histories, 2, median)
)
median_mae_history%>%ggplot(aes(x = epoch, y = validation_mae))+geom_line(col="red",size=1)+
theme_bw()+
geom_hline(yintercept = median(median_mae_history$validation_mae),linetype=2)

ggplot(median_mae_history, aes(x = epoch, y = validation_mae))+geom_smooth(col="red",fill="red")+
theme_bw()+
geom_hline(yintercept = median(median_mae_history$validation_mae),linetype=2)

Cuối cùng, ta dựng một mô hình chính thức với số epochs = 50 và batch_size=16
set.seed(123)
model <- build_model()
# Train it on the entirety of the data.
model %>% fit(train_data, train_targets,
epochs = 50, batch_size = 16, verbose = 0)
summary(model)
## ___________________________________________________________________________
## Layer (type) Output Shape Param #
## ===========================================================================
## dense_16 (Dense) (None, 64) 192
## ___________________________________________________________________________
## dense_17 (Dense) (None, 64) 4160
## ___________________________________________________________________________
## dense_18 (Dense) (None, 1) 65
## ===========================================================================
## Total params: 4,417
## Trainable params: 4,417
## Non-trainable params: 0
## ___________________________________________________________________________
result <- model %>% evaluate(test_data, test_targets)
result
## $loss
## [1] 0.6035802
##
## $mean_absolute_error
## [1] 0.6450254
So sánh hiệu năng 2 mô hình
Truth=testM$TLC
DNN=predict(model,test_data)
LMS=predict(m2,newdata=testM,type="response")
## new prediction
## New way of prediction in pb() (starting from GAMLSS version 5.0-3)
pdf=cbind(Truth,DNN,LMS)%>%as_data_frame()
colnames(pdf)=c("Truth","DeepNN","LMS")
pdf$Age=testM$Age
library(mlr)
regr.task= mlr::makeRegrTask(id = "dfM", data=testM, target = "TLC")
regr.lrn = makeLearner("regr.glm")
dummy=mlr::train(regr.lrn,regr.task)
dumpredDNN=predict(dummy,regr.task)
dumpredLMS=predict(dummy,regr.task)
dumpredDNN$data$response<-DNN
dumpredLMS$data$response<-LMS
Đây là hiệu năng mô hình Deep neural net với Tensorflo (Keras)
mets=list(mse,mae,medae,rmse)
performance(dumpredDNN,measures =mets)
## mse mae medae rmse
## 0.6035802 0.6450254 0.5570394 0.7769043
Còn đây là hiệu năng mô hình LMS của gamlss
performance(dumpredLMS,measures =mets)
## mse mae medae rmse
## 0.5183984 0.5819141 0.5102973 0.7199989
Màu xanh là mô hình LMS, màu đỏ là mô hình Deep neural network
pdf%>%ggplot()+geom_point(aes(x=Age,y=Truth),col="black",alpha=0.5)+
geom_smooth(aes(x=Age,y=DNN),col="red",fill="red",alpha=0.2)+
geom_smooth(aes(x=Age,y=LMS),col="blue",fill="blue",alpha=0.2)+theme_bw()+scale_y_continuous("TLC")
## `geom_smooth()` using method = 'loess'
## `geom_smooth()` using method = 'loess'

Trung bình, mô hình Deep neural network chỉ tiên lượng dung tích phổi sai khoảng 50 mL
mean(pdf$DeepNN-pdf$Truth)
## [1] -0.02100793
Nhận xét
Deep neural network (còn gọi là Deep learning) là một giải pháp vô cùng mạnh mẽ cho bài toán hồi quy, thậm chí nó tương đương với mô hình phi tuyến tính với hàm Spline bù trừ vốn là công cụ tối ưu nhất hiện nay cho các mô hình tăng trưởng trong y học lâm sàng.
Tuy nhiên, mô hình LMS có 2 ưu điểm mà Deep NN không thể thay thế, thứ nhất là tính tường minh: nội dung mô hình có thể được tính thủ công từ lookup table, thứ hai là khả năng ước tính được các ngưỡng giới hạn trên và dưới của giá trị bình thường trong quần thể từ 3 tham số L,M và S, cho phép diễn giải kết quả về mặt lâm sàng.Trong khi đó mô hình Deep NN không thể diễn giải được và chỉ có thể được áp dụng dưới dạng hard code trong firmware của thiết bị xét nghiệm.
Tuy nhiên, có thể dự báo trong tương lai giải pháp Deep learning sẽ cho phép những thiết bị xét nghiệm có khả năng tự tạo ra mô hình ước lượng giá trị tham chiếu cho chính nó mà không cần sự can thiệp của con người, và đây là một ứng dụng không thể xem thường.
LS0tDQp0aXRsZTogIkLDoGkgdG/DoW4gaOG7k2kgcXV5Ig0Kc3VidGl0bGU6ICJLZXJhcyB2LnMgR2FtbHNzIg0KYXV0aG9yOiAiTMOqIE5n4buNYyBLaOG6oyBOaGkiDQpkYXRlOiAiMDIgVGjDoW5nIDA1IDIwMTgiDQpvdXRwdXQ6DQogIGh0bWxfZG9jdW1lbnQ6IA0KICAgIGNvZGVfZG93bmxvYWQ6IHRydWUNCiAgICBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZGVmYXVsdCINCiAgICB0b2M6IFRSVUUNCiAgICB0b2NfZmxvYXQ6IFRSVUUNCi0tLQ0KDQpgYGB7ciBzZXR1cCxpbmNsdWRlPUZBTFNFfQ0Ka25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFKQ0KbGlicmFyeSh0aWR5dmVyc2UpDQpsaWJyYXJ5KHBhbmRlcikNCmBgYA0KDQohW10oS2VyYXN2c2dhbWxzcy5wbmcpDQoNCiMgR2nhu5tpIHRoaeG7h3UNCg0KVHJvbmcgYsOgaSB0aOG7sWMgaMOgbmggbmfhuq9uIG7DoHksIE5oaSBtdeG7kW4gbMOgbSBt4buZdCB0aMOtIG5naGnhu4dtIG5o4buPIMSR4buDIHNvIHPDoW5oIGto4bqjIG7Eg25nIGPhu6dhIDIgZ2nhuqNpIHRodeG6rXQga2jDoWMgbmhhdSBraGkgw6FwIGThu6VuZyBjaG8gY8O5bmcgbeG7mXQgYsOgaSB0b8OhbiBo4buTaSBxdXk6IG3hu5l0IGLDqm4gbMOgIE3DtCBow6xuaCBEZWVwIG5ldXJhbCBuZXR3b3JrIGLhurFuZyBUZW5zb3JmbG93IChnaWFvIHRo4bupYyBLZXJhcyksIGLDqm4ga2lhIGzDoCBtw7QgaMOsbmggTGFtYmRhLU11LVNpZ21hIChMTVMpIHRodeG7mWMgcGjGsMahbmcgcGjDoXAgR0FNTFNTLiBN4buXaSBsb+G6oWkgY8OzIHRo4buDIHhlbSBuaMawIOG7qW5nIGPhu60gdmnDqm4gbeG6oW5oIG5o4bqldCB04burIHRyxrDhu51uZyBwaMOhaSBXaGl0ZWJveGVzIHbDoCBCbGFja2JveGVzLg0KDQpUw6xuaCBodeG7kW5nIMSR4bq3dCByYSwgxJHDsyBsw6AgbeG7mXQgbmdoacOqbiBj4bupdSBk4buLY2ggdOG7hSBuaOG6sW0gxrDhu5tjIHTDrW5oIGdpw6EgdHLhu4sgZHVuZyBwaOG7lWkgYsOsbmggdGjGsOG7nW5nIChUTEMpIOG7nyBuYW0gZ2nhu5tpIHRodeG7mWMgY2jhu6duZyB04buZYyBDYXVjYXNpYW4uIA0KDQojIEThu68gbGnhu4d1DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeSh0aWR5dmVyc2UpDQoNCmRmPXJlYWQuY3N2KCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20va2lub2tvYmVydWppL1ItVHV0b3JpYWxzL21hc3Rlci9MTVNtb2RlbEdBTUxTUy5jc3YiLCBzZXA9IjsiKSU+JWFzX3RpYmJsZSgpDQoNCmRmTT1maWx0ZXIoZGYsU2V4PT0iTSIpJT4lZHBseXI6OnNlbGVjdCgtU2V4KQ0KDQpoZWFkKGRmTSklPiVrbml0cjo6a2FibGUoKQ0KYGBgDQoNCiMgTcO0IGjDrG5oIExNUyAoR2FtbHNzKQ0KDQpUcsaw4bubYyBo4bq/dCwgdGEgc+G6vSBkw7luZyBtw7QgaMOsbmggTE1TIHThu6sgcGFja2FnZSBnYW1sc3MuIEzDvSB0aHV54bq/dCB24buBIG3DtCBow6xuaCBMTVMgxJHDoyDEkcaw4bujYyBOaGkgZ2nhuqNpIHRow61jaCBj4bq3biBr4bq9IHRyb25nIGLDoGkgdGjhu7FjIGjDoG5oIHPhu5EgNyBzZXJpZXMgR2FtbHNzIDogPGh0dHA6Ly9ycHVicy5jb20vbGVuZ29ja2hhbmhpLzM0MjExMD4uIE3hu5l0IGPDoWNoIG5n4bqvbiBn4buNbiwgTE1TIGzDoCBt4buZdCB04bqtcCBo4bujcCAzIG3DtCBow6xuaCByacOqbmcgYmnhu4d0IE0sUyxMIGNobyAzIHRoYW0gc+G7kSA6IE11ICh24buLIHRy4buLIHRydW5nIHTDom0pLCBTaWdtYSAoU2NhbGUpIHbDoCBMYW1iZGEgKFNrZXduZXNzKSwgdMawxqFuZyDhu6luZyB24bubaSAzIHRoYW0gc+G7kSBNdSAobGluayBmdW5jdGlvbiA9IGxvZyksIFNpZ21hIChsaW5rPWxvZykgdsOgIE51IChsaW5rID0gaWRlbnRpdHkpIGPhu6dhIHF1eSBsdeG6rXQgcGjDom4gcGjhu5FpIEJveC1Db3gtQ29sZS1HcmVlbi4gTeG7l2kgbcO0IGjDrG5oIE0sTCxTIGzDoCBt4buZdCBtw7QgaMOsbmggR0FNIChnZW5lcmFsaXplZCBhZGRpdGl2ZSBtb2RlbCksIGNobyBwaMOpcCB0w61jaCBo4bujcCB0aMOqbSBTbW9vdGhpbmcgc3BsaW5lcyAoY8OhYyBow6BtIMSRaeG7gXUgaMOyYS9oaeG7h3UgY2jhu4luaCkgdsOgIGjDoG0gxJFhIHRo4bupYyAoUG9seW5vbWlhbCkuIENow61uaCBuaOG7r25nIGjDoG0gbsOgeSBjaG8gcGjDqXAgxJHhu5MgdGjhu4sgTE1TIG1vZGVsIHXhu5FuIGzGsOG7o24gbeG7gW0gbeG6oWkgdGhlbyBuaOG7r25nIGjDrG5oIGThuqFuZyBwaOG7qWMgdOG6oXAgbmjhuqV0IGPDsyB0aOG7gyB2w6AgdMSDbmcgxJHhu5kgY2jDrW5oIHjDoWMgY+G7p2EgbcO0IGjDrG5oLg0KDQrEkOG6p3UgdGnDqm4sIHRhIHPhur0gZMO5bmcgY2FyZXQgxJHhu4MgY2hpYSBk4buvIGxp4buHdSB0aMOgbmggMiBwaOG6p24sIDIwJSBkw6BuaCBjaG8ga2nhu4NtIMSR4buLbmggdsOgIDgwJSDEkeG7gyBk4buxbmcgbcO0IGjDrG5oOg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoY2FyZXQpDQoNCnNldC5zZWVkKDEyMykNCg0KaWRNPWNyZWF0ZURhdGFQYXJ0aXRpb24oeT1kZk0kVExDLCBwPTAuOCxsaXN0PUZBTFNFKQ0KdHJhaW5NPWRmTVtpZE0sXQ0KdGVzdE09ZGZNWy1pZE0sXQ0KYGBgDQoNClRhIGThu7FuZyBtw7QgaMOsbmggTE1TIGLhurFuZyBxdXkgdHLDrG5oIFN0ZXB3aXNlLCB0cm9uZyDEkcOzIHRhIHPhur0geMOhYyDEkeG7i25oIHThu5UgaOG7o3AgdOG7kWkgxrB1IGNobyAyIGPDonUgaOG7j2k6IEPDsyBj4bqnbiBtw7QgaMOsbmggY2hvIFNpZ21hL051IGhheSBraMO0bmcgPyB2w6A6IE7hur91IGPhuqduLCB0aMOsIG7hu5lpIGR1bmcgbcO0IGjDrG5oIGzDoCBnw6wgPw0KDQpUcm9uZyB0csaw4budbmcgaOG7o3AgbsOgeSBOaGkgbXXhu5FuIGNo4buNbiBs4buNYyBtw7QgaMOsbmggTCxNLFMgdOG7kWkgxrB1IGThu7FhIHbDoG8gMyBnaeG6oyDEkeG7i25oOiBow6BtIMSRYSB0aOG7qWMgYuG6rWMgMyBjaG8gSGVpZ2h0ICwgYuG6rWMgMiBjaG8gQWdlLCBrw6htIGhv4bq3YyBraMO0bmcga8OobSBTcGxpbmUgY2hvIEFnZSwgY8O0bmcgdGjhu6ljIG7DoHkgc+G6vSBs4bqnbiBsxrDhu6N0IHRlc3QgY2hvIE11IHbDoCBTaWdtYSwgTnUgxJHGsOG7o2MgbeG6t2MgxJHhu4tuaCBsw6AgaOG6sW5nIHPhu5EuDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KDQpsaWJyYXJ5KGdhbWxzcykNCm5DPC1kZXRlY3RDb3JlcygpDQoNCm0xPWdhbWxzcyhkYXRhPXRyYWluTSwNCiAgICAgICAgICBUTEN+MSwNCiAgICAgICAgICBzaWdtYS5mb3JtdWxhID0gVExDfjEsDQogICAgICAgICAgbnUuZm9ybXVsYSA9IFRMQ34xLA0KICAgICAgICAgIGZhbWlseT1CQ0NHKG11Lmxpbms9ImxvZyIpLA0KICAgICAgICAgIHRyYWNlPUZBTFNFLA0KICAgICAgICAgIHBhcmFsbGVsPSJtdWx0aWNvcmUiLA0KICAgICAgICAgIG5jcHVzID0gbkMpDQoNCm0yPXN0ZXBHQUlDQWxsLkEobTEsIHNjb3BlPWxpc3QobG93ZXI9fjEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB1cHBlcj1+cG9seShBZ2UsMikrcG9seShIZWlnaHQsMykrcGIoQWdlKSksDQogICAgICAgICAgICAgICAgIHNpZ21hLnNjb3BlID0gbGlzdChsb3dlcj1+MSwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHVwcGVyPX5wb2x5KEFnZSwyKStwb2x5KEhlaWdodCwzKStwYihBZ2UpKSwNCiAgICAgICAgICAgICAgICAgaz1sb2cobGVuZ3RoKHRyYWluTSkpLA0KICAgICAgICAgICAgICAgICB0cmFjZT1GQUxTRSwNCiAgICAgICAgICAgICAgICAgcGFyYWxsZWw9Im11bHRpY29yZSIsDQogICAgICAgICAgICAgICAgIG5jcHVzID0gbkMNCiAgICAgICAgICAgICAgKQ0KYGBgDQoNCkvhur90IHF14bqjIFN0ZXB3aXNlIG5oxrAgc2F1Og0KDQpNw7QgaMOsbmggY2hvIE11IGPDsyBjw7RuZyB0aOG7qWM6IE11IH4gcG9seShIZWlnaHQsMykrQWdlKyBNdVNwbGluZS4NCg0KTcO0IGjDrG5oIGNobyBTaWdtYSBjaOG7iSBn4buTbSBTaWdtYSB+IEFnZSArIFNpZ21TcGxpbi4NCg0KTnUgPSBo4bqxbmcgc+G7kS4NCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpzdW1tYXJ5KG0yKQ0KYGBgDQoNCiMgTcO0IGjDrG5oIERlZXAgbmV1cmFsIG5ldHdvcmsgduG7m2kgS2VyYXMNCg0KS2VyYXMgbMOgIG3hu5l0IG7hu4FuIHThuqNuZyBjaHV5w6puIGThu6VuZyBjaG8gRGVlcCBsZWFybmluZy4gQuG6o24gdGjhu4MgY+G7p2EgS2VyYXMgZMO5bmcgbmfDtG4gbmfhu68gUHl0aG9uLCBuaMawbmcgY8OzIGdpYW8gdGjhu6ljIMSR4buDIHPhu60gZOG7pW5nIHRyb25nIFIgKHBhY2thZ2Uga2VyYXMpLCBi4bqhbiBj4bqnbiBpbnN0YWxsIHRyxrDhu5tjIG3hu5l0IGfDs2kgUHl0aG9uIHRow60gZOG7pSBBbmFjb25kYSDEkeG7gyBjw7MgdGjhu4Mgc+G7rSBk4bulbmcga2VyYXMgdHJvbmcgUi4gICANCg0KR2lhbyB0aOG7qWMgS2VyYXMgdGnhur9wIG5o4bqtbiBt4buZdCBj4bqldSB0csO6YyBk4buvIGxp4buHdSDEkeG6t2MgYmnhu4d0IGfhu41pIGzDoCBjw6FjIHRlbnNvci4gVGEgY8OzIHRo4buDIGjDrG5oIGR1bmcgduG7gSB0ZW5zb3JzIG5oxrAga2jDoWkgbmnhu4dtIHThu5VuZyBxdcOhdCDEkeG7gyBn4buNaSB0w6puIG5o4buvbmcgxJHhu5FpIHTGsOG7o25nIGThu68gbGnhu4d1IG3DoCB0YSB04burbmcgYmnhur90IHRyb25nIFIgdGhlbyBz4buRIGNoaeG7gXUgdGjDtG5nIHRpbiAoZGltZW5zaW9uKS4gVHJvbmcgdGjDrSBk4bulIG7DoHksIHRhIGPDsyB0aOG7gyBn4buNaSB2ZWN0b3IgYmnhur9uIGvhur90IHF14bqjIGzDoCBsw6AgMUQgdGVuc29yLCBjw7JuIG1hdHJpeCBoYXkgZGF0YWZyYW1lIGfhu5NtIGPDoWMgZmVhdHVyZXMgxJHGsOG7o2MgZ+G7jWkgbMOgIDJEIHRlbnNvci4gDQoNClRyb25nIFIsIHRlbnNvciDEkcaw4bujYyBsxrB1IGTGsOG7m2kgZOG6oW5nIGFycmF5LCBkbyDEkcOzIHRyxrDhu5tjIGjhur90IHRhIGPhuqduIGhvw6FuIGNodXnhu4NuIG5o4buvbmcgZGF0YWZyYW1lIGhp4buHbiBjw7MgdGjDoG5oIGFycmF5LiDEkGnhu4NtIGtow6FjIGJp4buHdCB0aOG7qSBoYWkgxJHDsyBsw6AgS2VyYXMgdGnhur9wIG5o4bqtbiByacOqbmcgdGVuc29ycyBjaG8gdOG6rXAgZmVhdHVyZXMgdsOgIGNobyBiaeG6v24ga+G6v3QgcXXhuqMgbsOqbiB0YSBwaOG6o2kgY+G6r3QgMiBwaOG6p24gbsOgeSByacOqbmcgbOG6uyB04burIHThuq1wIHRyYWluIHbDoCB0ZXN0DQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KDQpsaWJyYXJ5KGtlcmFzKQ0KDQp0cmFpbl9kYXRhPC10cmFpbk0lPiVkcGx5cjo6c2VsZWN0KC0xKSU+JWFzLm1hdHJpeCgpJT4lYXMuYXJyYXkuZGVmYXVsdChkaW1uYW1lcz1OVUxMKSAgIyBDb252ZXJ0IGRhdGFmcmFtZSB0byBhcnJheQ0KZGltbmFtZXModHJhaW5fZGF0YSkgPC0gTlVMTA0KDQp0cmFpbl90YXJnZXRzPC10cmFpbk0lPiUuJFRMQyU+JWFzLmFycmF5KCkgDQoNCnRlc3RfZGF0YTwtdGVzdE0lPiVkcGx5cjo6c2VsZWN0KC0xKSU+JWFzLm1hdHJpeCgpJT4lYXMuYXJyYXkuZGVmYXVsdChkaW1uYW1lcz1OVUxMKSAgIyBDb252ZXJ0IGRhdGFmcmFtZSB0byBhcnJheQ0KZGltbmFtZXModHJhaW5fZGF0YSkgPC0gTlVMTA0KDQp0ZXN0X3RhcmdldHM8LXRlc3RNJT4lLiRUTEMlPiVhcy5hcnJheSgpIA0KDQpgYGANCg0KYGBge3J9DQpzdHIodHJhaW5fZGF0YSkNCnN0cih0ZXN0X2RhdGEpDQpzdHIodHJhaW5fdGFyZ2V0cykNCnN0cih0ZXN0X3RhcmdldHMpDQpgYGANCg0KTeG7mXQgxJFp4buDbSBj4bqnbiBsxrB1IMO9IHRp4bq/cCB0aGVvIMSRw7MgbMOgIE5ldXJhbCBuZXR3b3JrIGNo4buJIGhv4bqhdCDEkeG7mW5nIHThu5FpIMawdSBraGkgZOG7ryBsaeG7h3UgxJHhuqd1IHbDoG8gxJHGsOG7o2MgY2h14bqpbiBow7NhIGLhurFuZyBow6BtIHNjYWxlLCBz4butIGThu6VuZyB0cnVuZyBiw6xuaCBj4bunYSB04bqtcCB0cmFpbjoNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQptdSA8LSBhcHBseSh0cmFpbl9kYXRhLCAyLCBtZWFuKQ0Kc2lnbWEgPC0gYXBwbHkodHJhaW5fZGF0YSwgMiwgc2QpDQp0cmFpbl9kYXRhIDwtIHNjYWxlKHRyYWluX2RhdGEsIGNlbnRlciA9IG11LCBzY2FsZSA9IHNpZ21hKQ0KDQp0ZXN0X2RhdGEgPC0gc2NhbGUodGVzdF9kYXRhLCBjZW50ZXIgPSBtdSwgc2NhbGUgPSBzaWdtYSkNCg0KaGVhZCh0cmFpbl9kYXRhKSU+JWtuaXRyOjprYWJsZSgpDQpgYGANCg0KVHJvbmcgdGjDrSBk4bulIG7DoHksIGRvIGLDoGkgdG/DoW4gcuG6pXQgxJHGoW4gZ2nhuqNuIG7Dqm4gdGEgY2jhu4kgY+G6p24gZMO5bmcgbeG7mXQgbeG6oW5nIG5ldXJvbiBjw7MgY+G6pXUgdHLDumMgMiBs4bubcCBoaWRkZW4sIG3hu5dpIGzhu5twIGfhu5NtIDY0IMSRxqFuIHbhu4sgbmV1cm9ucy4gxJDhuqd1IHJhIGPhu6dhIG3huqFuZyBsxrDhu5tpIGzDoCAxIG5ldXJvbiBkdXkgbmjhuqV0ICh2w6Aga2jDtG5nIGvDqG0gYWN0aXZhdGlvbiBmdW5jdGlvbiBuaMawIHRyb25nIGLDoGkgdG/DoW4gdGnDqm4gbMaw4bujbmcgeMOhYyBzdeG6pXQpIOKAkyDEkeG7pyDEkeG7gyB4deG6pXQga+G6v3QgcXXhuqMgbMOgIG3hu5l0IGNvbiBz4buRIChzY2FsYXIpIHbDoCB0dXnhur9uIHTDrW5oIOKAkyBjaMOtbmggbMOgIGdpw6EgdHLhu4sgY+G6p24gxrDhu5tjIGzGsOG7o25nLg0KDQpN4bqhbmcgbMaw4bubaSDEkcaw4bujYyDEkcOzbmcgZ8OzaSB24bubaSBsb3NzIGZ1bmN0aW9uIGzDoCBtZWFuIHNxdWFyZWQgZXJyb3IgKHNhaSBz4buRIGLDrG5oIHBoxrDGoW5nIHRydW5nIGLDrG5oKSB2w6wgxJHDonkgbMOgIGLDoGkgdG/DoW4gaOG7k2kgcXV5LiBUcm9uZyBxdcOhIHRyw6xuaCBodeG6pW4gbHV54buHbiwgdGnDqnUgY2jDrSDEkcaw4bujYyBz4butIGThu6VuZyDEkeG7gyBj4bqjaSB0aGnhu4duIGhp4buHdSBuxINuZyBj4bunYSBtw7QgaMOsbmggbMOgIE1TRSAoc2FpIHPhu5EgdHV54buHdCDEkeG7kWkgdHJ1bmcgYsOsbmgpLiANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQpidWlsZF9tb2RlbCA8LSBmdW5jdGlvbigpIHsNCiAgbW9kZWwgPC0ga2VyYXNfbW9kZWxfc2VxdWVudGlhbCgpICU+JSANCiAgICBsYXllcl9kZW5zZSh1bml0cyA9IDY0LCBhY3RpdmF0aW9uID0gInJlbHUiLCANCiAgICAgICAgICAgICAgICBpbnB1dF9zaGFwZSA9IGRpbSh0cmFpbl9kYXRhKVtbMl1dKSAlPiUgDQogICAgbGF5ZXJfZGVuc2UodW5pdHMgPSA2NCwgYWN0aXZhdGlvbiA9ICJyZWx1IikgJT4lIA0KICAgIGxheWVyX2RlbnNlKHVuaXRzID0gMSkgDQogIA0KICBtb2RlbCAlPiUgY29tcGlsZSgNCiAgICBvcHRpbWl6ZXIgPSAicm1zcHJvcCIsIA0KICAgIGxvc3MgPSAibXNlIiwgDQogICAgbWV0cmljcyA9IGMoIm1hZSIpDQogICkNCn0NCmBgYA0KDQpUaeG6v3AgdGhlbywgdGEgdGjhu7FjIGhp4buHbiBt4buZdCBxdXkgdHLDrG5oIGtp4buDbSBjaOG7qW5nIGNow6lvIDUgYmxvY2tzIGzhurdwIGzhuqFpIDEwMCBs4bqnbg0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCg0Kc2V0LnNlZWQoMTIzKQ0KDQprIDwtIDUNCmluZGljZXMgPC0gc2FtcGxlKDE6bnJvdyh0cmFpbl9kYXRhKSkNCmZvbGRzIDwtIGN1dCgxOmxlbmd0aChpbmRpY2VzKSwgYnJlYWtzID0gaywgbGFiZWxzID0gRkFMU0UpIA0KDQpudW1fZXBvY2hzIDwtIDEwMA0KDQphbGxfbWFlX2hpc3RvcmllcyA8LSBOVUxMDQpmb3IgKGkgaW4gMTprKSB7DQogIGNhdCgicHJvY2Vzc2luZyBmb2xkICMiLCBpLCAiXG4iKQ0KICANCiAgIyBQcmVwYXJlIHRoZSB2YWxpZGF0aW9uIGRhdGE6IGRhdGEgZnJvbSBwYXJ0aXRpb24gIyBrDQogIHZhbF9pbmRpY2VzIDwtIHdoaWNoKGZvbGRzID09IGksIGFyci5pbmQgPSBUUlVFKQ0KICB2YWxfZGF0YSA8LSB0cmFpbl9kYXRhW3ZhbF9pbmRpY2VzLF0NCiAgdmFsX3RhcmdldHMgPC0gdHJhaW5fdGFyZ2V0c1t2YWxfaW5kaWNlc10NCiAgDQogICMgUHJlcGFyZSB0aGUgdHJhaW5pbmcgZGF0YTogZGF0YSBmcm9tIGFsbCBvdGhlciBwYXJ0aXRpb25zDQogIHBhcnRpYWxfdHJhaW5fZGF0YSA8LSB0cmFpbl9kYXRhWy12YWxfaW5kaWNlcyxdDQogIHBhcnRpYWxfdHJhaW5fdGFyZ2V0cyA8LSB0cmFpbl90YXJnZXRzWy12YWxfaW5kaWNlc10NCiAgDQogICMgQnVpbGQgdGhlIEtlcmFzIG1vZGVsIChhbHJlYWR5IGNvbXBpbGVkKQ0KICBtb2RlbCA8LSBidWlsZF9tb2RlbCgpDQogIA0KICAjIFRyYWluIHRoZSBtb2RlbCAoaW4gc2lsZW50IG1vZGUsIHZlcmJvc2U9MCkNCiAgaGlzdG9yeSA8LSBtb2RlbCAlPiUgZml0KA0KICAgIHBhcnRpYWxfdHJhaW5fZGF0YSwgcGFydGlhbF90cmFpbl90YXJnZXRzLA0KICAgIHZhbGlkYXRpb25fZGF0YSA9IGxpc3QodmFsX2RhdGEsIHZhbF90YXJnZXRzKSwNCiAgICBlcG9jaHMgPSBudW1fZXBvY2hzLCBiYXRjaF9zaXplID0gMSwgdmVyYm9zZSA9IDANCiAgKQ0KICBtYWVfaGlzdG9yeSA8LSBoaXN0b3J5JG1ldHJpY3MkdmFsX21lYW5fYWJzb2x1dGVfZXJyb3INCiAgYWxsX21hZV9oaXN0b3JpZXMgPC0gcmJpbmQoYWxsX21hZV9oaXN0b3JpZXMsIG1hZV9oaXN0b3J5KQ0KfQ0KDQpgYGANCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQptZWRpYW5fbWFlX2hpc3RvcnkgPC0gZGF0YS5mcmFtZSgNCiAgZXBvY2ggPSBzZXEoMTpuY29sKGFsbF9tYWVfaGlzdG9yaWVzKSksDQogIHZhbGlkYXRpb25fbWFlID0gYXBwbHkoYWxsX21hZV9oaXN0b3JpZXMsIDIsIG1lZGlhbikNCikNCg0KbWVkaWFuX21hZV9oaXN0b3J5JT4lZ2dwbG90KGFlcyh4ID0gZXBvY2gsIHkgPSB2YWxpZGF0aW9uX21hZSkpK2dlb21fbGluZShjb2w9InJlZCIsc2l6ZT0xKSsNCiAgdGhlbWVfYncoKSsNCiAgZ2VvbV9obGluZSh5aW50ZXJjZXB0ID0gbWVkaWFuKG1lZGlhbl9tYWVfaGlzdG9yeSR2YWxpZGF0aW9uX21hZSksbGluZXR5cGU9MikNCg0KZ2dwbG90KG1lZGlhbl9tYWVfaGlzdG9yeSwgYWVzKHggPSBlcG9jaCwgeSA9IHZhbGlkYXRpb25fbWFlKSkrZ2VvbV9zbW9vdGgoY29sPSJyZWQiLGZpbGw9InJlZCIpKw0KICB0aGVtZV9idygpKw0KICBnZW9tX2hsaW5lKHlpbnRlcmNlcHQgPSBtZWRpYW4obWVkaWFuX21hZV9oaXN0b3J5JHZhbGlkYXRpb25fbWFlKSxsaW5ldHlwZT0yKQ0KYGBgDQoNCkN14buRaSBjw7luZywgdGEgZOG7sW5nIG3hu5l0IG3DtCBow6xuaCBjaMOtbmggdGjhu6ljIHbhu5tpIHPhu5EgZXBvY2hzID0gNTAgdsOgIGJhdGNoX3NpemU9MTYNCg0KYGBge3IsbWVzc2FnZSA9IEZBTFNFLHdhcm5pbmc9RkFMU0V9DQoNCnNldC5zZWVkKDEyMykNCg0KbW9kZWwgPC0gYnVpbGRfbW9kZWwoKQ0KDQojIFRyYWluIGl0IG9uIHRoZSBlbnRpcmV0eSBvZiB0aGUgZGF0YS4NCm1vZGVsICU+JSBmaXQodHJhaW5fZGF0YSwgdHJhaW5fdGFyZ2V0cywNCiAgICAgICAgICAgICAgZXBvY2hzID0gNTAsIGJhdGNoX3NpemUgPSAxNiwgdmVyYm9zZSA9IDApDQoNCnN1bW1hcnkobW9kZWwpDQoNCmBgYA0KDQpgYGB7cixtZXNzYWdlID0gRkFMU0Usd2FybmluZz1GQUxTRX0NCnJlc3VsdCA8LSBtb2RlbCAlPiUgZXZhbHVhdGUodGVzdF9kYXRhLCB0ZXN0X3RhcmdldHMpDQoNCnJlc3VsdA0KYGBgDQoNCiMgU28gc8OhbmggaGnhu4d1IG7Eg25nIDIgbcO0IGjDrG5oDQoNCmBgYHtyLG1lc3NhZ2UgPSBGQUxTRSx3YXJuaW5nPUZBTFNFfQ0KVHJ1dGg9dGVzdE0kVExDDQpETk49cHJlZGljdChtb2RlbCx0ZXN0X2RhdGEpDQpMTVM9cHJlZGljdChtMixuZXdkYXRhPXRlc3RNLHR5cGU9InJlc3BvbnNlIikNCg0KcGRmPWNiaW5kKFRydXRoLEROTixMTVMpJT4lYXNfZGF0YV9mcmFtZSgpDQoNCmNvbG5hbWVzKHBkZik9YygiVHJ1dGgiLCJEZWVwTk4iLCJMTVMiKQ0KDQpwZGYkQWdlPXRlc3RNJEFnZQ0KDQpsaWJyYXJ5KG1scikNCg0KcmVnci50YXNrPSBtbHI6Om1ha2VSZWdyVGFzayhpZCA9ICJkZk0iLCBkYXRhPXRlc3RNLCB0YXJnZXQgPSAiVExDIikNCnJlZ3IubHJuID0gbWFrZUxlYXJuZXIoInJlZ3IuZ2xtIikNCg0KZHVtbXk9bWxyOjp0cmFpbihyZWdyLmxybixyZWdyLnRhc2spDQoNCmR1bXByZWRETk49cHJlZGljdChkdW1teSxyZWdyLnRhc2spDQpkdW1wcmVkTE1TPXByZWRpY3QoZHVtbXkscmVnci50YXNrKQ0KDQpkdW1wcmVkRE5OJGRhdGEkcmVzcG9uc2U8LUROTg0KZHVtcHJlZExNUyRkYXRhJHJlc3BvbnNlPC1MTVMNCmBgYA0KDQrEkMOieSBsw6AgaGnhu4d1IG7Eg25nIG3DtCBow6xuaCBEZWVwIG5ldXJhbCBuZXQgduG7m2kgVGVuc29yZmxvIChLZXJhcykNCg0KYGBge3J9DQptZXRzPWxpc3QobXNlLG1hZSxtZWRhZSxybXNlKQ0KcGVyZm9ybWFuY2UoZHVtcHJlZEROTixtZWFzdXJlcyA9bWV0cykNCmBgYA0KDQpDw7JuIMSRw6J5IGzDoCBoaeG7h3UgbsSDbmcgbcO0IGjDrG5oIExNUyBj4bunYSBnYW1sc3MNCg0KYGBge3J9DQpwZXJmb3JtYW5jZShkdW1wcmVkTE1TLG1lYXN1cmVzID1tZXRzKQ0KDQpgYGANCg0KTcOgdSB4YW5oIGzDoCBtw7QgaMOsbmggTE1TLCBtw6B1IMSR4buPIGzDoCBtw7QgaMOsbmggRGVlcCBuZXVyYWwgbmV0d29yaw0KDQpgYGB7cn0NCnBkZiU+JWdncGxvdCgpK2dlb21fcG9pbnQoYWVzKHg9QWdlLHk9VHJ1dGgpLGNvbD0iYmxhY2siLGFscGhhPTAuNSkrDQogIGdlb21fc21vb3RoKGFlcyh4PUFnZSx5PUROTiksY29sPSJyZWQiLGZpbGw9InJlZCIsYWxwaGE9MC4yKSsNCiAgZ2VvbV9zbW9vdGgoYWVzKHg9QWdlLHk9TE1TKSxjb2w9ImJsdWUiLGZpbGw9ImJsdWUiLGFscGhhPTAuMikrdGhlbWVfYncoKStzY2FsZV95X2NvbnRpbnVvdXMoIlRMQyIpDQpgYGANCg0KVHJ1bmcgYsOsbmgsIG3DtCBow6xuaCBEZWVwIG5ldXJhbCBuZXR3b3JrIGNo4buJIHRpw6puIGzGsOG7o25nIGR1bmcgdMOtY2ggcGjhu5VpIHNhaSBraG/huqNuZyA1MCBtTCANCg0KYGBge3J9DQptZWFuKHBkZiREZWVwTk4tcGRmJFRydXRoKQ0KYGBgDQoNCiMgTmjhuq1uIHjDqXQNCg0KRGVlcCBuZXVyYWwgbmV0d29yayAoY8OybiBn4buNaSBsw6AgRGVlcCBsZWFybmluZykgbMOgIG3hu5l0IGdp4bqjaSBwaMOhcCB2w7QgY8O5bmcgbeG6oW5oIG3hur0gY2hvIGLDoGkgdG/DoW4gaOG7k2kgcXV5LCB0aOG6rW0gY2jDrSBuw7MgdMawxqFuZyDEkcawxqFuZyB24bubaSBtw7QgaMOsbmggcGhpIHR1eeG6v24gdMOtbmggduG7m2kgaMOgbSBTcGxpbmUgYsO5IHRy4burIHbhu5FuIGzDoCBjw7RuZyBj4bulIHThu5FpIMawdSBuaOG6pXQgaGnhu4duIG5heSBjaG8gY8OhYyBtw7QgaMOsbmggdMSDbmcgdHLGsOG7n25nIHRyb25nIHkgaOG7jWMgbMOibSBzw6BuZy4NCg0KVHV5IG5oacOqbiwgbcO0IGjDrG5oIExNUyBjw7MgMiDGsHUgxJFp4buDbSBtw6AgRGVlcCBOTiBraMO0bmcgdGjhu4MgdGhheSB0aOG6vywgdGjhu6kgbmjhuqV0IGzDoCB0w61uaCB0xrDhu51uZyBtaW5oOiBu4buZaSBkdW5nIG3DtCBow6xuaCBjw7MgdGjhu4MgxJHGsOG7o2MgdMOtbmggdGjhu6cgY8O0bmcgdOG7qyBsb29rdXAgdGFibGUsIHRo4bupIGhhaSBsw6Aga2jhuqMgbsSDbmcgxrDhu5tjIHTDrW5oIMSRxrDhu6NjIGPDoWMgbmfGsOG7oW5nIGdp4bubaSBo4bqhbiB0csOqbiB2w6AgZMaw4bubaSBj4bunYSBnacOhIHRy4buLIGLDrG5oIHRoxrDhu51uZyB0cm9uZyBxdeG6p24gdGjhu4MgdOG7qyAzIHRoYW0gc+G7kSBMLE0gdsOgIFMsIGNobyBwaMOpcCBkaeG7hW4gZ2nhuqNpIGvhur90IHF14bqjIHbhu4EgbeG6t3QgbMOibSBzw6BuZy5Ucm9uZyBraGkgxJHDsyBtw7QgaMOsbmggRGVlcCBOTiBraMO0bmcgdGjhu4MgZGnhu4VuIGdp4bqjaSDEkcaw4bujYyB2w6AgY2jhu4kgY8OzIHRo4buDIMSRxrDhu6NjIMOhcCBk4bulbmcgZMaw4bubaSBk4bqhbmcgaGFyZCBjb2RlIHRyb25nIGZpcm13YXJlIGPhu6dhIHRoaeG6v3QgYuG7iyB4w6l0IG5naGnhu4dtLg0KDQpUdXkgbmhpw6puLCBjw7MgdGjhu4MgZOG7sSBiw6FvIHRyb25nIHTGsMahbmcgbGFpIGdp4bqjaSBwaMOhcCBEZWVwIGxlYXJuaW5nIHPhur0gY2hvIHBow6lwIG5o4buvbmcgdGhp4bq/dCBi4buLIHjDqXQgbmdoaeG7h20gY8OzIGto4bqjIG7Eg25nIHThu7EgdOG6oW8gcmEgbcO0IGjDrG5oIMaw4bubYyBsxrDhu6NuZyBnacOhIHRy4buLIHRoYW0gY2hp4bq/dSBjaG8gY2jDrW5oIG7DsyBtw6Aga2jDtG5nIGPhuqduIHPhu7EgY2FuIHRoaeG7h3AgY+G7p2EgY29uIG5nxrDhu51pLCB2w6AgxJHDonkgbMOgIG3hu5l0IOG7qW5nIGThu6VuZyBraMO0bmcgdGjhu4MgeGVtIHRoxrDhu51uZy4NCg==