0. Mục đích của xây dựng model

Đây là một mô hình phân loại. Bản chất của nó là để phân loại các user thành từng nhóm. Vì sao phải phân loại thành từng nhóm thì mục đích là như thế này:

Bản chất của Advosight là công cụ cho Influencer Marketing, thay vì tập trung vào 1 thị trường mục tiêu để quảng cáo thì lại đi tìm một đối tượng có sức ảnh hưởng (thông qua mạng xã hội). Dùng đối tượng này để nhắm đến được nhiều người mà họ có sức ảnh hưởng. Sau đó kết nối các đối tượng này với nhu cầu quảng cáo sản phẩm của các doanh nghiệp.

Mà doanh nghiệp ở Việt Nam theo thống kê đến năm 2016 là khoảng 500 ngàn doanh nghiệp, trong đó SME (Small and medium size) chiếm tới 98% phần trăm và chỉ có 2 phần trăm là các doanh nghiệp lớn.

Để mà kết nối được doanh nghiệp với đúng những Influencer thì cần những điều sau:

Thứ nhất là tìm ra được những Influencer và đo được độ quan trọng và ảnh hưởng của người đó trên mạng xã hội. Phần này đã được xây dựng trên Shiny App chính là những công thức để rank điểm số của mỗi người.

Thứ 2 là bài toán phân loại, bởi vì mỗi nhu cầu của doanh nghiệp nhắm đến đối tượng khách hàng khác nhau vì vậy họ cũng cần tìm được những Influencer mà đại diện được cho nhóm khách hàng mục tiêu của họ, tức là sản phẩm của họ nó hướng tới đến nhóm khách hàng riêng biệt.

Thì mô hình này sinh ra để giải quyết vấn đề thứ 2.

Ban đầu mục tiêu của mô hình này để phân loại ra thành từng nhóm như KOL, TOL, retailter, employee of retailer, fan (people love the product), thì mới đáp ứng được nhu cầu cho từng doang nghiệp, bởi vì mỗi mô hình doang nghiệp nhắm đến các đối tượng khác nhau. Không thể nào mà match 1 TOL (người có hiểu biết chuyên sâu về giải dụ như công nghệ) cho 1 doanh nghiệp sản xuất ra sản phẩm sữa.

Nhưng sau khi trao đổi với anh Lương Duy Bình thì anh ấy đang chỉ muốn tập trung vào Samsung nên mô hình thay đổi ở chỗ đánh Label. Bây giờ chỉ còn 3 nhóm là KOL (người nổi tiếng, showbiz), TOL (người am hiểu về 1 lĩnh vực chuyên biệt nào đó) và FAN (những người yêu thích sản phẩm, có share bài, hay post bài ở số lượng thấp với mức độ là tích cực)

Mô hình này được xây dựng để phân loại cho 3 nhóm là KOL, TOL và FAN

Xây dựng mô hình để tiết kiệm thời gian, tự động hóa, không phải cứ sau này cào về rồi lọc từng người xem ai ở nhóm nào sẽ rất là tốn công.

1. Giới thiệu về Random Forest

1.1 Random Forest

Ý tưởng của Random forest là : Từ 1 tập dữ liệu train data, ta chia nó ra nhiều sample nhỏ hơn, mỗi sample đại diện cho 1 tree và nhiều sample đó hợp lại tạo thành 1 rừng cây. Thì sự tổng hợp của những cây nhỏ và đơn giản này nó phản ánh được tính phức tạp của dữ liệu. Mỗi cây nhỏ này phản ánh được sự tinh túy của pattern trong kết quả được dựng nên mô hình Mỗi 1 sample đó là 1 cây hay 1 weak learner, nó giống như nguyên lý 1 team mạnh thì sẽ có nhiều sự phối hợp của những thành viên có những đặc điểm mạnh và chuyên biệt, như 1 team gồm có pháp sư, xạ thủ, support và đấu sĩ. Mỗi người có điểm mạnh yếu khác nhau, nhưng khi kết hợp lại thì sẽ tạo ra 1 team rất mạnh. Mỗi thành viên trong team sẽ có sự bổ trợ về kỹ năng cho nhau.

Việc tạo ra được sự đa dạng này là chìa khóa để tạo nên được mô hình phân loại mạnh mẽ. Thì theo cái nguyên lý này người ta gọi là ensemble method (based on the principle that weaker learners become stronger with teamwork). ensemble

Mỗi cây thì phải thực hiện việc dự báo phân loại, sau đó kết quả dự báo chung cho cả nhóm cây (group overall prediction) thì sẽ được quyết định bằng majority vote.

Mặc dù mỗi cây chỉ phản ánh 1 phần nhỏ của tổng thể dữ liệu, nhưng tổng thể thì được hoàn thiện bằng sự đa dạng ở mỗi khía cạnh.

Vậy để tạo ra sự đa dạng về đặc tính của mỗi cây nhỏ, (mỗi cây thể hiện được 1 khía cạnh thiết yếu), ta phải làm sao đưa được sự ngẫu nhiên (randomess) và mỗi cây nhỏ, nếu chia ra 100 cây giống nhau thì không có ý nghĩa gì, nhưng nếu ta chia ra được 100 cây khác nhau thì thuật toán sẽ bắt đầu có tác dụng.Việc đưa sư ngẫu nhiên (randomness) này sẽ được thực hiện bằng việc Bootstrap

Việc xây dưng randomforest thì giống với các quy tắc nền tảng khi xây dựng decision tree and bagging. Măc dù bagging cũng đã đưa sự randomness bằng phương pháp boostrap vào việc xây dựng cây phân loại để giảm đi sự sai lệch (variance) của kết quả dự đoán của mỗi cây con trong rừng cây và cải thiện quá trình thể hiện. Nhưng trees in bagging , thì mỗi cây (decision tree) nó không thực sự là độc lập với nhau (có tính correlation) bởi vì tất cả các biến gốc đều được xem xét khi thực hiện 1 quyết định phân tách của mỗi 1 cây con. Vấn đề nãy làm mỗi cây con có cùng 1 cấu trúc và nó ngăn cản mô hình tối ưu việc giảm thiểu độ sai số trong dự báo. Thì giải quyết cái viêc này bằng cách đưa thêm 1 tính randomness nữa là Split-varibale randomization. Tức là chia tách nhánh cây đầu tiên (root) dưa trên việc xem xét đa dạng hóa các biến một cách ngẫu nhiên. Vì vậy randomforest là phiên bản cải tiến của thuật toán Bagging

1.1.1 Ý tưởng của Boostrap

Boostrap là phương pháp tái chọn mẫu ngẫu nhiên và độc lập có trùng lặp từ tập dữ liệu train gốc thành nhiều tập mẫu hơn. Đơn thuần nó chỉ là việc lấy mẫu ngẫu nhiên có trùng lặp những hàng của tập dữ liệu train. Khi mà lấy mẫu có cho phép trùng lặp thì mỗi hàng có thể lặp lại nhiều lần và một số hàng thì bị vắng mặt. Cái ý tưởng của nó là muốn tạo ra một tập dữ liệu mới mà có giữ được một số những tính chất đặc trưng của tập train (dữ liệu gốc ban đầu) vì thế mà chúng ta có thể huấn luyện 1 mô hình giống nhau cho nhiều tập dữ liệu để lấy ra những đặc tính tiêu biểu nhất.

Có thể hiểu theo các bước sau:

Bước 1: Lấy m đối tượng ngẫu nhiên có trùng lặp từ trong n đối tượng ở tập train (dữ liệu gốc). m thì nhỏ hơn hoặc bằng n.

Bước 2: Huấn luyện cây quyết định trên những mẫu mới được tạo thành. Lặp lại những bước này bao nhiêu lần tùy thích. Càng nhiều cây thì mô hình càng tốt.

Bước 3: Giả sử hình trên là có 1000 cái cây, mỗi cái cây thì sẽ liên quan đến những đặc tính khác nhau của dữ liệu so với 1 cái cây cho toàn bộ dữ liệu và mỗi cái cây này sẽ có số điểm kết thúc khác nhau (terminal node), sau đó train mỗi cây và lấy giá trị trung bình của tổng tất cả các cây cho mỗi class để làm kết quả cuối cùng

1.1.2 Ý tưởng của Split-variable randomization

Thì cái thuật toán cây quyết định thì nó sẽ bắt đầu bằng cách tìm toàn bộ tất cả những biến đặc tính trong bootstrap sample và sau đó nó sẽ tìm cái biến nào là biến tốt nhất mà biến này có thể dùng để chia tách ra những nhóm có tính đồng nhất cao nhất. Tức là khả năng phân biệt lớn nhất. Thì vì cái nảy xảy ra như nhau ở từng cây thì nó sẽ dẫn đến sự không mấy khác biệt nhau mấy về cấu trúc ở mỗi cây quyết định trong rừng cây dẫn đến sự gọi là tree correlation như đã nói ở trên. Vì vậy thuật toán random forest phải khống chế cái chuyện này bằng cách là không đi tìm toàn bộ những biến đặc tính mà chỉ chọn từng cụm biến ngẫu nhiên trong tổng số biến, rồi đi tìm ra biến tốt nhất trong cụm đó để thực hiện việc chia tách.

Kết hợp lại cả 2 ý tưởng bagging bootstrap, Split-variable randomization thì gọi là random forest

2. Thang đo mức độ thể hiện của mô hình Random Forest

Confusion Matrix: A breakdown of predictions into a table showing correct predictions (the diagonal) and the types of incorrect predictions made (what classes incorrect predictions were assigned).

Precision: A measure of a classifiers exactness.

Recall: A measure of a classifiers completeness

F1 Score (or F-score): A weighted average of precision and recall.

ROC Curves: Like precision and recall, accuracy is divided into sensitivity and specificity and models can be chosen based on the balance thresholds of these values.

3. Phần tiền xử lý dữ liệu và thống kê về cấu trúc bảng dữ liệu

Gọi các thư viện cần thiết để làm việc

# load tools
library(tidyverse)    # for handlinf data
library(dplyr)        # manipulating data
library(readxl)       # read xlsx file
library(rsample)      # data splitting 
library(randomForest) # basic implementation
library(ranger)       # a faster implementation of randomForest
library(caret)        # an aggregator package for performing many machine learning models
library(h2o)          # an extremely fast java-based platform
library(ggplot2)      # for visualization
library(pROC)         # for calculate multiple class AUC and plot it
library(randomForestExplainer) 
library(knitr)
library(kableExtra)
library(xlsx)

Chỉnh lại quy ước hiển thị số theo ngôn ngữ khoa học

# set to scientific notation
options("scipen"=0, "digits"=7)

Load dữ liệu và format dữ liệu

# load and format data
full = read.xlsx('full.xlsx', sheetIndex = 1) # read data with label and meta information
full1 = full[-which(is.na(full)),] # remove unavailabel content
full1$user_id = as.character(full1$user_id) # convert user_id from factor to character type of sample data
check = read.csv('features.csv')# load the data with necessary featuresd
colnames(check)[2] = 'user_id' # rename the user id to the consensus name with names in label data
check = check[,-1]
check$user_id = as.character(check$user_id) # convert user_id from factor to character type of database data
full2 = inner_join(full1, check, by = 'user_id') # join the sample data with database data, intersection joined by 'user_id' 
full3 = full2[-which(full2$Label == 'NA'), ] # remove the NA label
full3$Label = as.character(full3$Label)
full3 = full3[,-1]

Xem qua thống kê về dữ liệu

print(dim(full3))
[1] 786  32

Dữ liệu hiện tại có 822 hàng và 33 cột

Xem qua 10 đối tượng ban đầu có số lượng like trên tổng số post cao nhất

ten_people = full3 %>% arrange(desc(totalLikesPosts))
#head(ten_people[, -1], 10)  %>% kable() %>% kable_styling(bootstrap_options = "striped", font_size = 8)
print(ten_people[, -1])

Xem qua cấu trúc dữ liệu và định dạng của từng biến

print(str(full3[, -(1:4)]))
'data.frame':   786 obs. of  28 variables:
 $ totalPosts          : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalPosPosts       : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalNegPosts       : int  0 0 0 0 0 0 0 0 0 0 ...
 $ word_count_post     : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalLikesPosts     : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalSharesPosts    : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalCommentsPosts  : int  0 0 0 0 0 0 0 0 0 0 ...
 $ images_count_post   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ videos_count_post   : int  0 0 0 0 0 0 0 0 0 0 ...
 $ link_yes_no         : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalShares         : int  1 1 1 2 1 1 1 15 1 3 ...
 $ totalPosShares      : int  0 0 0 1 0 0 0 1 0 0 ...
 $ totalNegShares      : int  0 0 0 0 0 0 0 0 0 0 ...
 $ word_count_share    : int  24 7 8 1 0 0 0 5 0 0 ...
 $ totalLikesShares    : int  0 0 0 0 0 0 0 0 0 6 ...
 $ totalCommentsShares : int  0 0 0 0 0 0 0 0 0 0 ...
 $ images_count_share  : int  0 0 0 0 0 0 0 0 0 0 ...
 $ videos_count_share  : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalComments       : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalPosComments    : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalNegComments    : int  0 0 0 0 0 0 0 0 0 0 ...
 $ word_count_comments : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalLikesComments  : int  0 0 0 0 0 0 0 0 0 0 ...
 $ totalRepliesComments: int  0 0 0 0 0 0 0 0 0 0 ...
 $ images_count_comment: int  0 0 0 0 0 0 0 0 0 0 ...
 $ videos_count_comment: int  0 0 0 0 0 0 0 0 0 0 ...
 $ total_follower      : int  0 0 0 0 0 0 0 0 0 0 ...
 $ total_friends       : int  0 0 0 0 0 0 0 0 0 0 ...
NULL

Xem qua thống kê về mỗi biến đặc trưng của tập dữ liệu

print(summary(full3[,-(1:6)]))
 totalNegPosts      word_count_post   totalLikesPosts   totalSharesPosts  totalCommentsPosts images_count_post
 Min.   :0.000000   Min.   :    0.0   Min.   :      0   Min.   :    0.0   Min.   :    0.0    Min.   :  0.000  
 1st Qu.:0.000000   1st Qu.:    0.0   1st Qu.:      0   1st Qu.:    0.0   1st Qu.:    0.0    1st Qu.:  0.000  
 Median :0.000000   Median :    0.0   Median :      0   Median :    0.0   Median :    0.0    Median :  0.000  
 Mean   :0.002544   Mean   :  318.4   Mean   :   9980   Mean   :  130.8   Mean   :  371.2    Mean   :  2.753  
 3rd Qu.:0.000000   3rd Qu.:    0.0   3rd Qu.:      0   3rd Qu.:    0.0   3rd Qu.:    0.0    3rd Qu.:  0.000  
 Max.   :1.000000   Max.   :51146.0   Max.   :4896526   Max.   :41638.0   Max.   :79872.0    Max.   :354.000  
 videos_count_post  link_yes_no       totalShares     totalPosShares    totalNegShares     word_count_share 
 Min.   : 0.0000   Min.   :  0.000   Min.   :   0.0   Min.   :  0.000   Min.   :0.000000   Min.   :   0.00  
 1st Qu.: 0.0000   1st Qu.:  0.000   1st Qu.:   1.0   1st Qu.:  0.000   1st Qu.:0.000000   1st Qu.:   0.00  
 Median : 0.0000   Median :  0.000   Median :   1.0   Median :  0.000   Median :0.000000   Median :   0.00  
 Mean   : 0.3753   Mean   :  4.939   Mean   :  13.4   Mean   :  1.711   Mean   :0.001272   Mean   :  16.56  
 3rd Qu.: 0.0000   3rd Qu.:  0.000   3rd Qu.:   2.0   3rd Qu.:  0.000   3rd Qu.:0.000000   3rd Qu.:   0.00  
 Max.   :70.0000   Max.   :893.000   Max.   :1636.0   Max.   :257.000   Max.   :1.000000   Max.   :1770.00  
 totalLikesShares  totalCommentsShares images_count_share videos_count_share totalComments     
 Min.   :   0.00   Min.   : 0.0000     Min.   : 0.00000   Min.   : 0.00000   Min.   :    0.00  
 1st Qu.:   0.00   1st Qu.: 0.0000     1st Qu.: 0.00000   1st Qu.: 0.00000   1st Qu.:    0.00  
 Median :   0.00   Median : 0.0000     Median : 0.00000   Median : 0.00000   Median :    0.00  
 Mean   :  10.67   Mean   : 0.1361     Mean   : 0.08906   Mean   : 0.05089   Mean   :   33.29  
 3rd Qu.:   0.00   3rd Qu.: 0.0000     3rd Qu.: 0.00000   3rd Qu.: 0.00000   3rd Qu.:    0.00  
 Max.   :2169.00   Max.   :27.0000     Max.   :15.00000   Max.   :15.00000   Max.   :14071.00  
 totalPosComments   totalNegComments   word_count_comments totalLikesComments totalRepliesComments
 Min.   :   0.000   Min.   : 0.00000   Min.   :     0      Min.   :   0.000   Min.   :  0.0000    
 1st Qu.:   0.000   1st Qu.: 0.00000   1st Qu.:     0      1st Qu.:   0.000   1st Qu.:  0.0000    
 Median :   0.000   Median : 0.00000   Median :     0      Median :   0.000   Median :  0.0000    
 Mean   :   6.723   Mean   : 0.03053   Mean   :  1318      Mean   :   3.188   Mean   :  0.3613    
 3rd Qu.:   0.000   3rd Qu.: 0.00000   3rd Qu.:     0      3rd Qu.:   0.000   3rd Qu.:  0.0000    
 Max.   :2792.000   Max.   :10.00000   Max.   :513841      Max.   :1302.000   Max.   :211.0000    
 images_count_comment videos_count_comment total_follower      total_friends   
 Min.   :  0.000      Min.   : 0.00000     Min.   :        0   Min.   :   0.0  
 1st Qu.:  0.000      1st Qu.: 0.00000     1st Qu.:        0   1st Qu.:   0.0  
 Median :  0.000      Median : 0.00000     Median :        0   Median :   0.0  
 Mean   :  1.131      Mean   : 0.05216     Mean   :   266803   Mean   : 124.5  
 3rd Qu.:  0.000      3rd Qu.: 0.00000     3rd Qu.:        0   3rd Qu.:   0.0  
 Max.   :648.000      Max.   :35.00000     Max.   :158378908   Max.   :5000.0  

4. Tạo tập train set và test set trên dữ liệu full3

Trước hết gieo hạt bằng 13 để sau này mỗi lần chạy có một kết quả giống nhau. Vì random forest chạy bằng bootstrap cho nên phải gieo hạt để có thể tái hiện lại kết quả khi ngẫu nhiên chọn mẫu.

set.seed(13)

Phân chia dữ liệu theo tỉ lệ 7:3, 7 cho train set và 3 cho test set

# create a random index for every row of data frame
train_data_index = sample(1: nrow(full3), round(nrow(full3)*.7)) # create a random index in 818 observations, with the 7:3 ratio
# create trainning data and format
train_data = full3[train_data_index,]
train_data = train_data %>% select(-user_id, -Name, -Description, -totalLikesPosts, -totalCommentsPosts, -images_count_post)
train_data$Label = as.character(train_data$Label)
train_data$Label = as.factor(train_data$Label)
# create testing data and format
test_data = full3[-train_data_index,]
test_data = test_data %>% select(-user_id, -Name, -Description, -totalLikesPosts, -totalCommentsPosts, -images_count_post)
test_data$Label = as.character(test_data$Label)
test_data$Label = as.factor(test_data$Label)

Xem lại kích thước của mỗi tập

Train set

print(dim(train_data))
[1] 550  26

Test set

print(dim(test_data))
[1] 236  26

5. Xem số cây ở khoảng bao nhiêu thì độ sai số của mô hình giảm xuống nhiều nhất.

Bởi vì mỗi cây con trong random forest thì được huấn luyện dựa trên mẫu được tạo bởi phương pháp bootstrap (tái chọn mẫu nhỏ hơn trên tập dữ liệu train data gốc) cho nên một số đối tượng trong mẫu được tạo sẽ bị trùng lặp và một số đối tượng sẽ bị loại ra. Thì tập những cái đối tượng bị loại ra sẽ được gọi là OUT OF BAG samples và sẽ được dùng như một tập validation set.

Bởi vì cái tập OOB này không được dùng để huấn luyện những cái cây, cho nên nó sẽ được dùng để đánh giá khả năng thể hiện của mô hình cây trên dữ liệu chưa được tiếp cận đến.

Dùng thang đo OOB error rate để đánh giá xem số cây cần chia ra là bao nhiêu để giảm độ sai số của mô hình xuống nhỏ nhất. Thì đây là error rate mà được tính từ những sample mà không được lựa chọn ra khi thực hiện việc tái chọn mẫu(bootstrap, random sampling)

Search trong khoảng 2000 cây.

# find the number of tree 
set.seed(13)
  # defautl rf model with 2000 trees
  m1 = randomForest(
    formula = Label~. ,
    data = train_data,
    ntree = 2000
  )
  # number of tree with the error rate , plotting
  m1 # 2000 trees

Call:
 randomForest(formula = Label ~ ., data = train_data, ntree = 2000) 
               Type of random forest: classification
                     Number of trees: 2000
No. of variables tried at each split: 5

        OOB estimate of  error rate: 1.82%
Confusion matrix:
    FAN KOL TOL class.error
FAN 494   1   0 0.002020202
KOL   0  34   1 0.028571429
TOL   3   5  12 0.400000000
  plot(m1, main = 'error rate with 2000 trees')
  legend("topright", colnames(m1$err.rate),col=1:4,cex=0.8,fill=1:4)

  
  # convert form matrix to data frame
  err.df_2000 = m1$err.rate %>% as.data.frame()
  #find the number of tree that correspondent to min error rate
  which.min(err.df_2000$OOB)
[1] 39
  

Nhìn vào hình và kết quả trả về ta thấy mức error rate nhỏ nhất ứng với mức là 181 cây.

7. Chạy thử model với các tham số ở mức mặc định và số cây là 181 cây.

# SIMPLE MODEL
# model data
set.seed(13)
rf = randomForest(Label ~ . , data = train_data, importance = T, ntree = 181 )
# compare the predicted label with the actual label
result_rf = predict(rf, newdata = test_data)
confusionMatrix(result_rf, test_data$Label, mode = 'everything')
Confusion Matrix and Statistics

          Reference
Prediction FAN KOL TOL
       FAN 211   2   1
       KOL   1  10   4
       TOL   1   2   4

Overall Statistics
                                          
               Accuracy : 0.9534          
                 95% CI : (0.9181, 0.9765)
    No Information Rate : 0.9025          
    P-Value [Acc > NIR] : 0.003117        
                                          
                  Kappa : 0.7362          
 Mcnemar's Test P-Value : 0.801252        

Statistics by Class:

                     Class: FAN Class: KOL Class: TOL
Sensitivity              0.9906    0.71429    0.44444
Specificity              0.8696    0.97748    0.98678
Pos Pred Value           0.9860    0.66667    0.57143
Neg Pred Value           0.9091    0.98190    0.97817
Precision                0.9860    0.66667    0.57143
Recall                   0.9906    0.71429    0.44444
F1                       0.9883    0.68966    0.50000
Prevalence               0.9025    0.05932    0.03814
Detection Rate           0.8941    0.04237    0.01695
Detection Prevalence     0.9068    0.06356    0.02966
Balanced Accuracy        0.9301    0.84588    0.71561
# calculate the auc
actual = as.numeric(test_data$Label )
predicted = as.numeric(result_rf)
result_roc = multiclass.roc(actual, predicted)
result_roc

Call:
multiclass.roc.default(response = actual, predictor = predicted)

Data: predicted with 3 levels of actual: 1, 2, 3.
Multi-class area under the curve: 0.835
auc(result_roc)
Multi-class area under the curve: 0.835

Xem error rate thay đổi theo số cây tương ứng

# plot the error rate and number of tree
plot(rf, main = 'Learning curve')
legend("topright", colnames(rf$err.rate),col=1:4,cex=0.8,fill=1:4)

Xem các biến quan trọng đóng góp vào mô hình dự đoán

varImpPlot(rf, main = 'Important variable', cex = .7)

Trên hình trên ta có thể thấy 5 biến quan trọng đóng góp vào mô hình là :

rf %>% important_variables(k=5,ties_action = "draw")
[1] "total_follower"  "word_count_post" "totalPosPosts"   "totalPosts"      "link_yes_no"    

Xem thêm các đối tượng nào bị phân loại sai

which(result_rf != test_data$Label)
 [1] 155 189 191 199 200 202 203 204 205 208 209
error = full3[-train_data_index,][which(result_rf != test_data$Label), ]
error$predict_label = result_rf[which(result_rf != test_data$Label)]
compare = error %>% select(user_id, Name, Description, Label, predict_label)
# kable(compare) %>% kable_styling(bootstrap_options = "striped", font_size = 8)
# kable(error) %>% kable_styling(bootstrap_options = "striped", font_size = 8)
print(compare)
print(error[, -1])

8. Kết luận

Ở đây ta có thể thấy là độ chính xác của mô hình phân loại cho 2 class là FAN là rất cao.

Class FAN có độ chính xác Precision : 0.986 và mức độ không bỏ sót Recall: là 0988

Class KOL có độ chính xác Precision : 0.6667 và mức độ không bỏ sót Recall: là 0.71

Còn yếu nhất là class TOL với độ chính xác là 0.571 nhưng mức độ không bỏ sót chỉ có 0.57

Chính vì thế độ chính xác Accuracy : 0.9534 là vô nghĩa, không phải thang đo chính xác, nó đã bị đẩy cao lên do sự mất cân bằng giữ tỉ lệ các class. Trong đó FAN và KOL chiếm gần 97 phần trăm (mà 2 class này lại phân loại đúng nhất). (Xem phụ lục ở dưới)

print(prop.table(table(full3$Label)))

       FAN        KOL        TOL 
0.90076336 0.06234097 0.03689567 

Nhìn vào chỉ số No Information Rate chúng ta thấy, chọn ngẫu nhiên thì độ chính xác trong 100 lần chọn 90 phần trăm trong khi đó mô hình chỉ thể hiện sự cải thiện được 5 phần trăm. Thì mô hình thế này còn kém

Area under ROC curve (AUC) : 0.853 phản ánh được phần nào sự thiếu chính xác của mô hình với sự mất cân bằng tỉ lệ giữa các class KOL, TOL và FAN

Mà vì sao lại có sự phân loại sai là do không có nhiều dữ liệu để cho máy học về TOL và KOL.

9. Sự mất cân bằng tỉ lệ giữa các nhóm Label

Xem lại sự tỉ lệ và phân bố của nhãn dán

unique(full3$Label)
[1] "FAN" "KOL" "TOL"
print(table(full3$Label))

FAN KOL TOL 
708  49  29 

Phân phối biểu diễn

ggplot(data = full3,
       aes(Label)) +
  geom_bar(aes(fill = Label))

Nhìn vào trên hình ta có thể thấy sự mất cân bằng của nhãn FAN so với các nhóm còn lại là KOL và TOL Tỉ lệ thực tế 3 nhóm chiếm là

print(prop.table(table(full3$Label)))

       FAN        KOL        TOL 
0.90076336 0.06234097 0.03689567 

10. Giải quyết sự mất cân bằng giữa các class KOL, TOL so với FAN và cải thiện mô hình.

Bởi vì mô hình nó quá chênh lệch giữa các class như đã nói ở mục 9. Mà yêu cầu là cần nhiều dữ liệu cho nên phương pháp giải quyết vấn đề này là dùng phương pháp Oversampling.

Mà cụ thể là dùng kĩ thuật SMOTE: Synthetic Minority Over-sampling Technique. Kỹ thuật này dùng để tạo ra những fake user thuộc các nhóm chiếm tỉ lệ thấp như KOL và TOL

Để diễn giải vấn đề này thì xin dùng 1 small data set rất nổi tiếng là IRIS để diễn giải. Iris là tập dữ liệu gồm các loại hoa là Setosa, versicolor và virginica và các đặc tính của các loại hoa như là độ dài của đài hoa ….

smote = iris[c(2, 9, 16, 23, 51:63),
         c(1, 2, 5)]
smote$Species = as.character(smote$Species)
print(smote)
table(smote$Species)

    setosa versicolor 
         4         13 

Ta có thể thấy loại hoa thuộc nhóm setosa chiếm số lượng 4, 0.23 phần trăm, rất là mất cân bằng so với versicolor

Như hình minh họa ở trên, loại hoa setosa, vòng tròn mà đỏ là nhóm thiểu số (Minority) mất cân bằng so với nhóm màu xanh versicolor (Majority class). Để mô hình dự báo đúng hơn, ta sẽ tạo thêm những điểm màu đỏ sao cho cân bằng với điểm màu xanh.

Thì thuật toán SMOTE sẽ vẽ các đường nối các điểm đó lại với nhau như hình dưới

Vì sao tạo được các đường nối nhau thì xin giải thích như sau:

Thuật toán sẽ đi tìm nhóm chiếm tỉ lệ thấp nhất, sau đó đó nó sẽ chọn lấy 1 điểm gốc mà điểm đó gấn nhất với tất cả các điểm còn lại. Sau đó chúng ta sẽ chọn bán kính vùng chúng ta muốn lấy các điểm dữ liệu bằng cách cho biết số điểm gần nhất có thể có so với điểm ban đầu. Sau đó nối chúng lại với nhau.

Cụ thể như hình dưới, chúng ta muốn có những điểm dữ liệu giả nằm trong bán kính từ điểm gốc đến 1 điểm gần nhất so với nó

hoặc là từ điểm gốc đến 2 điểm gần nhất so với nó

Và tạo các điểm dữ liệu trên các đường này như hình dưới

Sau đây là code để tạo thuật toán

Thì có thể nhận thấy ở trên các biến đầu vào X chính là các ma trận chứa các biến đặc tính ở hàng cột, target chính là nhãn của các đối tượng, K chính là số điểm gần nhất so với điểm gốc. dup_size chính là số điểm mới cần được tạo bằng tổng số điểm gốc cộng với các điểm gần nó nhất nhân với hệ số dup_size

Sau đây sẽ chạy tạo dữ liệu giả của các nhóm bị mất cân bằng bằng thư viện smotefamily

library(smotefamily)

dat = full3 %>% select(-user_id, -Name, -Description, -Label)

set.seed(13)
# create a TOL synth
tol_bal = SMOTE(dat,
                as.numeric(as.factor(full3$Label)),
                K = 10,
                dup_size = 11)
tol_synth = tol_bal$syn_data

# add the label for TOL class
tol_synth$user_id = 'fake id'
tol_synth$Name = 'fake name'
tol_synth$Description = 'fake description'
tol_synth$Label = 'TOL'
tol_synth = tol_synth[, -29]


# create a KOL synth
dat2 = tol_bal$data

kol_bal = SMOTE(dat2[, -29],
                dat2$class,
                K = 10,
                dup_size = 9 )

kol_synth = kol_bal$syn_data

# add the label for KOL class
kol_synth$user_id = 'fake id'
kol_synth$Name = 'fake name'
kol_synth$Description = 'fake description'
kol_synth$Label = 'KOL'
kol_synth = kol_synth[, -29]

# create a full new data
balance_data = bind_rows(full3, tol_synth, kol_synth)
table(balance_data$Label)

11. Train model mới với dữ liệu mới

Sau khi đã tạo dữ liệu giả để khắc phục sự mất cân bằng giữa các class ta load dữ liệu vào máy

# load data
dat = read.xlsx('class_balanced.xlsx', sheetIndex = 1) # read data with label and meta information
dat = dat[,-1]

Bởi vì có những thang đo không chuẩn nên ta phải đi quy đổi lại, vì đây là biến quan trọng như đã nêu ra ở mục 7, ví dụ như tổng số like trên tất cả phải lấy thành trung bình số like trên một post, tương tự cho comment và image trên Post

# adjust the variable for more objectivity
dat = dat %>% mutate(likesPerPost = totalLikesPosts / (totalPosts +1 ),
                     commentsPerPost =totalCommentsPosts / (totalPosts + 1),
                     imagesPerPost = images_count_post / (totalPosts + 1))

Chia thành tập train set và test set, tỉ lê 7 : 3.

set.seed(13)
train_data_index2 = sample(1: nrow(dat), round(nrow(dat)*.7)) # create a random index in 818 observations, with the 7:3 ratio
# create trainning data and format
train_data2 = dat[train_data_index2,]
train_data2 = train_data2 %>% select(-user_id, -Name, -Description, -totalLikesPosts, -totalCommentsPosts, -images_count_post)
train_data2$Label = as.character(train_data2$Label)
train_data2$Label = as.factor(train_data2$Label)
# create testing data and format
test_data2 = dat[-train_data_index2,]
test_data2 = test_data2 %>% select(-user_id, -Name, -Description, -totalLikesPosts, -totalCommentsPosts, -images_count_post)
test_data2$Label = as.character(test_data2$Label)
test_data2$Label = as.factor(test_data2$Label)

Chạy mô hình với các tham số mặc đinh chỉ thay đổi số cây bằng 1000 cây.

set.seed(13)
m_1000 = randomForest(Label ~ . , data = train_data2, importance = T, ntree = 1000 )
# compare the predicted label with the actual label
result_m_1000 = predict(m_1000, newdata = test_data2)
metric = confusionMatrix(result_m_1000, test_data2$Label, mode = 'everything')

12. Đánh giá kết quả của mô hình mới

Kết quả của tập train set

print(m_1000)

Call:
 randomForest(formula = Label ~ ., data = train_data2, importance = T,      ntree = 1000) 
               Type of random forest: classification
                     Number of trees: 1000
No. of variables tried at each split: 5

        OOB estimate of  error rate: 1.26%
Confusion matrix:
    FAN KOL TOL class.error
FAN 522   0   0 0.000000000
KOL   1 346   2 0.008595989
TOL   3   8 225 0.046610169

Ta có thể thấy được độ chính xác trên tập train set là rất cao, không có sai sot cho nhóm FAN, và hầu như là cũng thế cho nhóm KOL. Chỉ có nhóm TOL là sai sót khoảng 0.046. Và tổng error rate ước tính là 1.26 phần trăm trên tập OOB.

Kết quả trên tập test set

print(metric)
Confusion Matrix and Statistics

          Reference
Prediction FAN KOL TOL
       FAN 222   0   1
       KOL   0 139   3
       TOL   0   2 108

Overall Statistics
                                          
               Accuracy : 0.9874          
                 95% CI : (0.9727, 0.9954)
    No Information Rate : 0.4674          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.9802          
 Mcnemar's Test P-Value : NA              

Statistics by Class:

                     Class: FAN Class: KOL Class: TOL
Sensitivity              1.0000     0.9858     0.9643
Specificity              0.9960     0.9910     0.9945
Pos Pred Value           0.9955     0.9789     0.9818
Neg Pred Value           1.0000     0.9940     0.9890
Precision                0.9955     0.9789     0.9818
Recall                   1.0000     0.9858     0.9643
F1                       0.9978     0.9823     0.9730
Prevalence               0.4674     0.2968     0.2358
Detection Rate           0.4674     0.2926     0.2274
Detection Prevalence     0.4695     0.2989     0.2316
Balanced Accuracy        0.9980     0.9884     0.9794

Confusion matrix

table(test_data2$Label)

FAN KOL TOL 
222 141 112 
metric$table
          Reference
Prediction FAN KOL TOL
       FAN 222   0   1
       KOL   0 139   3
       TOL   0   2 108

Thì ta có thể thấy là 222 đối tượng được gắn nhãn là FAN đã được phân loại chính xác tuyệt đối.

có 141 đối tượng được gắn nhẵn KOL chỉ có 2 đối tượng bị phân loại chệch ra thành TOL.

Và 112 đối tượng được gắn nhẵn TOL chỉ có 4 đối tượng bị phân loại thành 1 cho FAN và 3 cho KOL

Thống kê chung về mô hình

print(prop.table(table(test_data2$Label)))

      FAN       KOL       TOL 
0.4673684 0.2968421 0.2357895 

Mô hình này có độ chính xác Accuracy rate là 0.987 rất là cao

Khi so sánh với lại với chỉ số No Information Rate, như đã nói ở mục 8. Chỉ số này cho thấy, nếu không xét đến bất kể thông tin nào ngoài phân bố tỉ lệ của 3 nhóm FAN, KOL và TOL thì nếu chọn ngẫu nhiên, mỗi 100 lần thì có 46 lần là chọn được trúng FAN, 30 lần là cọn được trúng KOL và 23 lần là TOL .

Độ chính xác của mô hình Accuracy rate là 0.987 cho thấy mô hình đang đi đúng hướng và làm việc rất tốt.

Precison và Recall

Ở đây ta có thể thấy là độ chính xác của mô hình phân loại cho 3 class là FAN, KOL và TOL là rất cao.

Class FAN có độ chính xác Precision : 0.9955 và mức độ không bỏ sót Recall: là 1, có nghĩa là máy nó đã lọc ra 100 phần trăm các đối tượng là FAN (Recall) và Phân loại chính xác đến 99,5 phần trăm là (precision)

Class KOL có độ chính xác Precision : 0.9789 và mức độ không bỏ sót Recall: là 0.985

Class TOL với độ chính xác là 0.9818 và mức độ không bỏ sót là: 0.9643

Thì cái điều này cho thấy mô hình hoạt động rất chính xác

Các đối tượng bị phân loại sai

error_1000 = dat[-train_data_index2,][which(result_m_1000 != test_data2$Label), ]
error_1000$predict_label = result_m_1000[which(result_m_1000 != test_data2$Label)]
compare_1000 = error_1000 %>% select(user_id, Name, Description, Label, predict_label)
print(compare_1000)
print(error_1000)

AUC (Area under ROC curve)

# calculate the auc
actual_m_1000 = as.numeric(test_data2$Label )
predicted_m_1000 = as.numeric(result_m_1000)
result_roc_m_1000 = multiclass.roc(actual_m_1000, predicted_m_1000)
result_roc_m_1000

Call:
multiclass.roc.default(response = actual_m_1000, predictor = predicted_m_1000)

Data: predicted_m_1000 with 3 levels of actual_m_1000: 1, 2, 3.
Multi-class area under the curve: 0.9887
auc(result_roc_m_1000)
Multi-class area under the curve: 0.9887

Ở đây ta thấy độ chính xác tổng hợp cho 3 class là 0.9887 là rất tốt

Có thể coi hình sau:

# plot the auc
rs <- result_roc_m_1000[['rocs']]
plot.roc(rs[[1]], main = "AUC of FAN")

plot.roc(rs[[2]], main = "AUC of KOL")

plot.roc(rs[[3]], main = "AUC of TOL")

plot.roc(rs[[3]], main = "AUC of 3 classes")
sapply(1:length(rs),function(i) lines.roc(rs[[i]],col=i, lwd = .8))
                   [,1]        [,2]        [,3]       
percent            FALSE       FALSE       FALSE      
sensitivities      Numeric,4   Numeric,4   Numeric,4  
specificities      Numeric,4   Numeric,4   Numeric,4  
thresholds         Numeric,4   Numeric,4   Numeric,4  
direction          "<"         "<"         "<"        
cases              Numeric,141 Numeric,112 Numeric,112
controls           Numeric,222 Numeric,222 Numeric,141
fun.sesp           ?           ?           ?          
call               Expression  Expression  Expression 
original.predictor Numeric,475 Numeric,475 Numeric,475
original.response  Numeric,475 Numeric,475 Numeric,475
predictor          Numeric,363 Numeric,334 Numeric,253
response           Numeric,363 Numeric,334 Numeric,253
levels             Character,2 Character,2 Character,2

14. Tối ưu hóa mô hình

Các biến quan trọng

varImpPlot(m_1000, cex =.8)

Learning curve

  plot(m_1000, main = 'error rate with 1000 trees')
  legend("topright", colnames(m_1000$err.rate),col=1:4,cex=0.8,fill=1:4)

# convert form matrix to data frame
err.df = m_1000$err.rate %>% as.data.frame()
#find the number of tree that correspondent to min error rate
which.min(err.df$OOB)
[1] 121

Ở đây có thể thấy sai số thấp nhất ở 121 cây

Chạy mô hình mới với 121 cây với các tham số mặc định

set.seed(13)
m_121 = randomForest(Label ~ . , data = train_data2, importance = T, ntree = 121 )
# compare the predicted label with the actual label
result_m_121 = predict(m_121, newdata = test_data2)
metric_121 = confusionMatrix(result_m_121, test_data2$Label, mode = 'everything')
print(metric_121)
Confusion Matrix and Statistics

          Reference
Prediction FAN KOL TOL
       FAN 222   0   1
       KOL   0 139   3
       TOL   0   2 108

Overall Statistics
                                          
               Accuracy : 0.9874          
                 95% CI : (0.9727, 0.9954)
    No Information Rate : 0.4674          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.9802          
 Mcnemar's Test P-Value : NA              

Statistics by Class:

                     Class: FAN Class: KOL Class: TOL
Sensitivity              1.0000     0.9858     0.9643
Specificity              0.9960     0.9910     0.9945
Pos Pred Value           0.9955     0.9789     0.9818
Neg Pred Value           1.0000     0.9940     0.9890
Precision                0.9955     0.9789     0.9818
Recall                   1.0000     0.9858     0.9643
F1                       0.9978     0.9823     0.9730
Prevalence               0.4674     0.2968     0.2358
Detection Rate           0.4674     0.2926     0.2274
Detection Prevalence     0.4695     0.2989     0.2316
Balanced Accuracy        0.9980     0.9884     0.9794

Thực hiện grid search để tìm các tham số tối ưu

# full grid search

hyper_grid <- expand.grid(
  mtry       = seq(2, 28, by = 1),
  node_size  = seq(2, 28, by = 1),
  sample_size = c(.55, .632, .70, .80),
  OOB_RMSE   = 0
)

# searching

for(i in 1:nrow(hyper_grid)) {
  
  # train model
  model <- ranger(
    formula         = Label ~ ., 
    data            = train_data2, 
    num.trees       = 121,
    mtry            = hyper_grid$mtry[i],
    min.node.size   = hyper_grid$node_size[i],
    sample.fraction = hyper_grid$sample_size[i],
    seed            = 12
  )
  
  # add OOB error to grid
  hyper_grid$OOB_RMSE[i] <- sqrt(model$prediction.error)
}

Xem các tham số tối ưu nhất

# look at lowest RMSE 
hyper_grid %>% 
  dplyr::arrange(OOB_RMSE) %>%
  head(10)

Kết quả cho thấy sau khi thực hiện tìm kiếm trên 2916 lần chạy mô hình với các tham số ta được bộ tham số có OOB thấp nhất là

mtry =3

node_size = 2

Ta chạy mô hình mới với các tham số tối ưu vừa tìm được

set.seed(13)
optimized_model = randomForest(formula = Label ~., 
                               data = train_data2,
                               ntree = 121,
                               mtry = 3,
                               nodesize = 3
                               
                               )
m_op = predict(optimized_model, newdata = test_data2)
confusionMatrix(m_op, test_data2$Label, mode = 'everything')
Confusion Matrix and Statistics

          Reference
Prediction FAN KOL TOL
       FAN 222   0   1
       KOL   0 139   3
       TOL   0   2 108

Overall Statistics
                                          
               Accuracy : 0.9874          
                 95% CI : (0.9727, 0.9954)
    No Information Rate : 0.4674          
    P-Value [Acc > NIR] : < 2.2e-16       
                                          
                  Kappa : 0.9802          
 Mcnemar's Test P-Value : NA              

Statistics by Class:

                     Class: FAN Class: KOL Class: TOL
Sensitivity              1.0000     0.9858     0.9643
Specificity              0.9960     0.9910     0.9945
Pos Pred Value           0.9955     0.9789     0.9818
Neg Pred Value           1.0000     0.9940     0.9890
Precision                0.9955     0.9789     0.9818
Recall                   1.0000     0.9858     0.9643
F1                       0.9978     0.9823     0.9730
Prevalence               0.4674     0.2968     0.2358
Detection Rate           0.4674     0.2926     0.2274
Detection Prevalence     0.4695     0.2989     0.2316
Balanced Accuracy        0.9980     0.9884     0.9794

Model mới kết quả cũng không hơn gì model cũ với tham số mặc định và chạy với 1000 cây

LS0tCnRpdGxlOiAiUmVwb3J0IgpEYXRlIDogJ1NlcCAybmQsIDIwMTgnCkZyb20gOiAiRGFjIERpbmggLSBEYXRhU2NpZW5jZSAtIEt5YW5vbiBMYWIiCm91dHB1dDogaHRtbF9ub3RlYm9vawpzZWxmLWNvbnRhaW5lZDogeWVzCi0tLQoKIyMgMC4gTeG7pWMgxJHDrWNoIGPhu6dhIHjDonkgZOG7sW5nIG1vZGVsCgrEkMOieSBsw6AgbeG7mXQgbcO0IGjDrG5oIHBow6JuIGxv4bqhaS4gQuG6o24gY2jhuqV0IGPhu6dhIG7DsyBsw6AgxJHhu4MgcGjDom4gbG/huqFpIGPDoWMgdXNlciB0aMOgbmggdOG7q25nIG5ow7NtLiBWw6wgc2FvIHBo4bqjaSBwaMOibiBsb+G6oWkgdGjDoG5oIHThu6tuZyBuaMOzbSB0aMOsIG3hu6VjIMSRw61jaCBsw6AgbmjGsCB0aOG6vyBuw6B5OgoKQuG6o24gY2jhuqV0IGPhu6dhIEFkdm9zaWdodCBsw6AgY8O0bmcgY+G7pSBjaG8gSW5mbHVlbmNlciBNYXJrZXRpbmcsIHRoYXkgdsOsIHThuq1wIHRydW5nIHbDoG8gMSB0aOG7iyB0csaw4budbmcgbeG7pWMgdGnDqnUgxJHhu4MgcXXhuqNuZyBjw6FvIHRow6wgbOG6oWkgxJFpIHTDrG0gbeG7mXQgxJHhu5FpIHTGsOG7o25nIGPDsyBz4bupYyDhuqNuaCBoxrDhu59uZyAodGjDtG5nIHF1YSBt4bqhbmcgeMOjIGjhu5lpKS4gRMO5bmcgxJHhu5FpIHTGsOG7o25nIG7DoHkgxJHhu4Mgbmjhuq9tIMSR4bq/biDEkcaw4bujYyBuaGnhu4F1IG5nxrDhu51pIG3DoCBo4buNIGPDsyBz4bupYyDhuqNuaCBoxrDhu59uZy4gU2F1IMSRw7Mga+G6v3QgbuG7kWkgY8OhYyDEkeG7kWkgdMaw4bujbmcgbsOgeSB24bubaSBuaHUgY+G6p3UgcXXhuqNuZyBjw6FvIHPhuqNuIHBo4bqpbSBj4bunYSBjw6FjIGRvYW5oIG5naGnhu4dwLgoKTcOgIGRvYW5oIG5naGnhu4dwIOG7nyBWaeG7h3QgTmFtIHRoZW8gdGjhu5FuZyBrw6ogxJHhur9uIG7Eg20gMjAxNiBsw6Aga2hv4bqjbmcgNTAwIG5nw6BuIGRvYW5oIG5naGnhu4dwLCB0cm9uZyDEkcOzIFNNRSAoU21hbGwgYW5kIG1lZGl1bSBzaXplKSBjaGnhur9tIHThu5tpIDk4JSBwaOG6p24gdHLEg20gdsOgIGNo4buJIGPDsyAyIHBo4bqnbiB0csSDbSBsw6AgY8OhYyBkb2FuaCBuZ2hp4buHcCBs4bubbi4KCsSQ4buDIG3DoCBr4bq/dCBu4buRaSDEkcaw4bujYyBkb2FuaCBuZ2hp4buHcCB24bubaSDEkcO6bmcgbmjhu69uZyBJbmZsdWVuY2VyIHRow6wgY+G6p24gbmjhu69uZyDEkWnhu4F1IHNhdToKClRo4bupIG5o4bqldCBsw6AgdMOsbSByYSDEkcaw4bujYyBuaOG7r25nIEluZmx1ZW5jZXIgdsOgIMSRbyDEkcaw4bujYyDEkeG7mSBxdWFuIHRy4buNbmcgdsOgIOG6o25oIGjGsOG7n25nIGPhu6dhIG5nxrDhu51pIMSRw7MgdHLDqm4gbeG6oW5nIHjDoyBo4buZaS4gUGjhuqduIG7DoHkgxJHDoyDEkcaw4bujYyB4w6J5IGThu7FuZyB0csOqbiBTaGlueSBBcHAgY2jDrW5oIGzDoCBuaOG7r25nIGPDtG5nIHRo4bupYyDEkeG7gyByYW5rIMSRaeG7g20gc+G7kSBj4bunYSBt4buXaSBuZ8aw4budaS4KClRo4bupIDIgbMOgIGLDoGkgdG/DoW4gcGjDom4gbG/huqFpLCBi4bufaSB2w6wgbeG7l2kgbmh1IGPhuqd1IGPhu6dhIGRvYW5oIG5naGnhu4dwIG5o4bqvbSDEkeG6v24gxJHhu5FpIHTGsOG7o25nIGtow6FjaCBow6BuZyBraMOhYyBuaGF1IHbDrCB24bqteSBo4buNIGPFqW5nIGPhuqduIHTDrG0gxJHGsOG7o2Mgbmjhu69uZyBJbmZsdWVuY2VyIG3DoCDEkeG6oWkgZGnhu4duIMSRxrDhu6NjIGNobyBuaMOzbSBraMOhY2ggaMOgbmcgbeG7pWMgdGnDqnUgY+G7p2EgaOG7jSwgdOG7qWMgbMOgIHPhuqNuIHBo4bqpbSBj4bunYSBo4buNIG7DsyBoxrDhu5tuZyB04bubaSDEkeG6v24gbmjDs20ga2jDoWNoIGjDoG5nIHJpw6puZyBiaeG7h3QuCgpUaMOsIG3DtCBow6xuaCBuw6B5IHNpbmggcmEgxJHhu4MgZ2nhuqNpIHF1eeG6v3QgduG6pW4gxJHhu4EgdGjhu6kgMi4KCkJhbiDEkeG6p3UgbeG7pWMgdGnDqnUgY+G7p2EgbcO0IGjDrG5oIG7DoHkgxJHhu4MgcGjDom4gbG/huqFpIHJhIHRow6BuaCB04burbmcgbmjDs20gbmjGsCBLT0wsIFRPTCwgcmV0YWlsdGVyLCBlbXBsb3llZSBvZiByZXRhaWxlciwgZmFuIChwZW9wbGUgbG92ZSB0aGUgcHJvZHVjdCksIHRow6wgbeG7m2kgxJHDoXAg4bupbmcgxJHGsOG7o2Mgbmh1IGPhuqd1IGNobyB04burbmcgZG9hbmcgbmdoaeG7h3AsIGLhu59pIHbDrCBt4buXaSBtw7QgaMOsbmggZG9hbmcgbmdoaeG7h3Agbmjhuq9tIMSR4bq/biBjw6FjIMSR4buRaSB0xrDhu6NuZyBraMOhYyBuaGF1LiBLaMO0bmcgdGjhu4MgbsOgbyBtw6AgbWF0Y2ggMSBUT0wgKG5nxrDhu51pIGPDsyBoaeG7g3UgYmnhur90IGNodXnDqm4gc8OidSB24buBIGdp4bqjaSBk4bulIG5oxrAgY8O0bmcgbmdo4buHKSBjaG8gMSBkb2FuaCBuZ2hp4buHcCBz4bqjbiB4deG6pXQgcmEgc+G6o24gcGjhuqltIHPhu69hLgoKTmjGsG5nIHNhdSBraGkgdHJhbyDEkeG7lWkgduG7m2kgYW5oIEzGsMahbmcgRHV5IELDrG5oIHRow6wgYW5oIOG6pXkgxJFhbmcgY2jhu4kgbXXhu5FuIHThuq1wIHRydW5nIHbDoG8gU2Ftc3VuZyBuw6puIG3DtCBow6xuaCB0aGF5IMSR4buVaSDhu58gY2jhu5cgxJHDoW5oIExhYmVsLgpCw6J5IGdp4budIGNo4buJIGPDsm4gMyBuaMOzbSBsw6AgS09MIChuZ8aw4budaSBu4buVaSB0aeG6v25nLCBzaG93Yml6KSwgVE9MIChuZ8aw4budaSBhbSBoaeG7g3UgduG7gSAxIGzEqW5oIHbhu7FjIGNodXnDqm4gYmnhu4d0IG7DoG8gxJHDsykgdsOgIEZBTiAobmjhu69uZyBuZ8aw4budaSB5w6p1IHRow61jaCBz4bqjbiBwaOG6qW0sIGPDsyBzaGFyZSBiw6BpLCBoYXkgcG9zdCBiw6BpIOG7nyBz4buRIGzGsOG7o25nIHRo4bqlcCB24bubaSBt4bupYyDEkeG7mSBsw6AgdMOtY2ggY+G7sWMpCgpNw7QgaMOsbmggbsOgeSDEkcaw4bujYyB4w6J5IGThu7FuZyDEkeG7gyBwaMOibiBsb+G6oWkgY2hvIDMgbmjDs20gbMOgIEtPTCwgVE9MIHbDoCBGQU4KCljDonkgZOG7sW5nIG3DtCBow6xuaCDEkeG7gyB0aeG6v3Qga2nhu4dtIHRo4budaSBnaWFuLCB04buxIMSR4buZbmcgaMOzYSwga2jDtG5nIHBo4bqjaSBj4bupIHNhdSBuw6B5IGPDoG8gduG7gSBy4buTaSBs4buNYyB04burbmcgbmfGsOG7nWkgeGVtIGFpIOG7nyBuaMOzbSBuw6BvIHPhur0gcuG6pXQgbMOgIHThu5FuIGPDtG5nLiAKCgojIyAxLiBHaeG7m2kgdGhp4buHdSB24buBIFJhbmRvbSBGb3Jlc3QKCiMjIyAxLjEgUmFuZG9tIEZvcmVzdAoKw50gdMaw4bufbmcgY+G7p2EgUmFuZG9tIGZvcmVzdCBsw6AgOgpU4burIDEgdOG6rXAgZOG7ryBsaeG7h3UgdHJhaW4gZGF0YSwgdGEgY2hpYSBuw7MgcmEgbmhp4buBdSBzYW1wbGUgbmjhu48gaMahbiwgbeG7l2kgc2FtcGxlIMSR4bqhaSBkaeG7h24gY2hvIDEgdHJlZSB2w6Agbmhp4buBdSBzYW1wbGUgxJHDsyBo4bujcCBs4bqhaSB04bqhbyB0aMOgbmggMSBy4burbmcgY8OieS4gVGjDrCBz4buxIHThu5VuZyBo4bujcCBj4bunYSBuaOG7r25nIGPDonkgbmjhu48gdsOgIMSRxqFuIGdp4bqjbiBuw6B5IG7DsyBwaOG6o24gw6FuaCDEkcaw4bujYyB0w61uaCBwaOG7qWMgdOG6oXAgY+G7p2EgZOG7ryBsaeG7h3UuIE3hu5dpIGPDonkgbmjhu48gbsOgeSBwaOG6o24gw6FuaCDEkcaw4bujYyBz4buxIHRpbmggdMO6eSBj4bunYSBwYXR0ZXJuIHRyb25nIGvhur90IHF14bqjIMSRxrDhu6NjIGThu7FuZyBuw6puIG3DtCBow6xuaApN4buXaSAxIHNhbXBsZSDEkcOzIGzDoCAxIGPDonkgaGF5IDEgd2VhayBsZWFybmVyLCBuw7MgZ2nhu5FuZyBuaMawIG5ndXnDqm4gbMO9IDEgdGVhbSBt4bqhbmggdGjDrCBz4bq9IGPDsyBuaGnhu4F1IHPhu7EgcGjhu5FpIGjhu6NwIGPhu6dhIG5o4buvbmcgdGjDoG5oIHZpw6puIGPDsyBuaOG7r25nIMSR4bq3YyDEkWnhu4NtIG3huqFuaCB2w6AgY2h1ecOqbiBiaeG7h3QsIG5oxrAgMSB0ZWFtIGfhu5NtIGPDsyBwaMOhcCBzxrAsIHjhuqEgdGjhu6csIHN1cHBvcnQgdsOgIMSR4bqldSBzxKkuCk3hu5dpIG5nxrDhu51pIGPDsyDEkWnhu4NtIG3huqFuaCB54bq/dSBraMOhYyBuaGF1LCBuaMawbmcga2hpIGvhur90IGjhu6NwIGzhuqFpIHRow6wgc+G6vSB04bqhbyByYSAxIHRlYW0gcuG6pXQgbeG6oW5oLiBN4buXaSB0aMOgbmggdmnDqm4gdHJvbmcgdGVhbSBz4bq9IGPDsyBz4buxIGLhu5UgdHLhu6MgduG7gSBr4bu5IG7Eg25nIGNobyBuaGF1LgoKVmnhu4djIHThuqFvIHJhIMSRxrDhu6NjIHPhu7EgxJFhIGThuqFuZyBuw6B5IGzDoCBjaMOsYSBraMOzYSDEkeG7gyB04bqhbyBuw6puIMSRxrDhu6NjIG3DtCBow6xuaCBwaMOibiBsb+G6oWkgbeG6oW5oIG3hur0uClRow6wgdGhlbyBjw6FpIG5ndXnDqm4gbMO9IG7DoHkgbmfGsOG7nWkgdGEgZ+G7jWkgbMOgIGVuc2VtYmxlIG1ldGhvZCAoYmFzZWQgb24gdGhlIHByaW5jaXBsZSB0aGF0IHdlYWtlciBsZWFybmVycyBiZWNvbWUgc3Ryb25nZXIgd2l0aCB0ZWFtd29yaykuCiFbZW5zZW1ibGVdKGltYWdlcy9lbnNlbWJsZV9tZXRob2QucG5nKQoKTeG7l2kgY8OieSB0aMOsIHBo4bqjaSB0aOG7sWMgaGnhu4duIHZp4buHYyBk4buxIGLDoW8gcGjDom4gbG/huqFpLCBzYXUgxJHDsyBr4bq/dCBxdeG6oyBk4buxIGLDoW8gY2h1bmcgY2hvIGPhuqMgbmjDs20gY8OieSAoZ3JvdXAgb3ZlcmFsbCBwcmVkaWN0aW9uKSB0aMOsIHPhur0gxJHGsOG7o2MgcXV54bq/dCDEkeG7i25oIGLhurFuZyBtYWpvcml0eSB2b3RlLgoKIVtdKGltYWdlcy9tYWpvcml0eV92b3RlLmpwZykKCk3hurdjIGTDuSBt4buXaSBjw6J5IGNo4buJIHBo4bqjbiDDoW5oIDEgcGjhuqduIG5o4buPIGPhu6dhIHThu5VuZyB0aOG7gyBk4buvIGxp4buHdSwgbmjGsG5nIHThu5VuZyB0aOG7gyB0aMOsIMSRxrDhu6NjIGhvw6BuIHRoaeG7h24gYuG6sW5nIHPhu7EgxJFhIGThuqFuZyDhu58gbeG7l2kga2jDrWEgY+G6oW5oLgoKVuG6rXkgxJHhu4MgdOG6oW8gcmEgc+G7sSDEkWEgZOG6oW5nIHbhu4EgxJHhurdjIHTDrW5oIGPhu6dhIG3hu5dpIGPDonkgbmjhu48sICht4buXaSBjw6J5IHRo4buDIGhp4buHbiDEkcaw4bujYyAxIGtow61hIGPhuqFuaCB0aGnhur90IHnhur91KSwgdGEgcGjhuqNpIGzDoG0gc2FvIMSRxrBhIMSRxrDhu6NjIHPhu7Egbmfhuqt1IG5oacOqbiAocmFuZG9tZXNzKSB2w6AgbeG7l2kgY8OieSBuaOG7jywgbuG6v3UgY2hpYSByYSAxMDAgY8OieSBnaeG7kW5nIG5oYXUgdGjDrCBraMO0bmcgY8OzIMO9IG5naMSpYSBnw6wsIG5oxrBuZyBu4bq/dSB0YSBjaGlhIHJhIMSRxrDhu6NjIDEwMCBjw6J5IGtow6FjIG5oYXUgdGjDrCB0aHXhuq10IHRvw6FuIHPhur0gYuG6r3QgxJHhuqd1IGPDsyB0w6FjIGThu6VuZy5WaeG7h2MgxJHGsGEgc8awIG5n4bqrdSBuaGnDqm4gKHJhbmRvbW5lc3MpIG7DoHkgc+G6vSDEkcaw4bujYyB0aOG7sWMgaGnhu4duIGLhurFuZyB2aeG7h2MgKipCb290c3RyYXAqKgoKVmnhu4djIHjDonkgZMawbmcgcmFuZG9tZm9yZXN0IHRow6wgZ2nhu5FuZyB24bubaSBjw6FjIHF1eSB04bqvYyBu4buBbiB04bqjbmcga2hpIHjDonkgZOG7sW5nIGRlY2lzaW9uIHRyZWUgYW5kIGJhZ2dpbmcuIE3Eg2MgZMO5IGJhZ2dpbmcgY8WpbmcgxJHDoyDEkcawYSBz4buxIHJhbmRvbW5lc3MgYuG6sW5nIHBoxrDGoW5nIHBow6FwIGJvb3N0cmFwIHbDoG8gdmnhu4djIHjDonkgZOG7sW5nIGPDonkgcGjDom4gbG/huqFpIMSR4buDIGdp4bqjbSDEkWkgc+G7sSBzYWkgbOG7h2NoICh2YXJpYW5jZSkgY+G7p2Ega+G6v3QgcXXhuqMgZOG7sSDEkW/DoW4gY+G7p2EgbeG7l2kgY8OieSBjb24gdHJvbmcgcuG7q25nIGPDonkgdsOgIGPhuqNpIHRoaeG7h24gcXXDoSB0csOsbmggdGjhu4MgaGnhu4duLiBOaMawbmcgdHJlZXMgaW4gYmFnZ2luZyAsIHRow6wgbeG7l2kgY8OieSAoZGVjaXNpb24gdHJlZSkgbsOzIGtow7RuZyB0aOG7sWMgc+G7sSBsw6AgxJHhu5ljIGzhuq1wIHbhu5tpIG5oYXUgKGPDsyB0w61uaCBjb3JyZWxhdGlvbikgYuG7n2kgdsOsICB04bqldCBj4bqjIGPDoWMgYmnhur9uIGfhu5FjIMSR4buBdSDEkcaw4bujYyB4ZW0geMOpdCBraGkgdGjhu7FjIGhp4buHbiAxIHF1eeG6v3QgxJHhu4tuaCBwaMOibiB0w6FjaCBj4bunYSBt4buXaSAxIGPDonkgY29uLiBW4bqlbiDEkeG7gSBuw6N5IGzDoG0gbeG7l2kgY8OieSBjb24gY8OzIGPDuW5nIDEgY+G6pXUgdHLDumMgdsOgIG7DsyBuZ8SDbiBj4bqjbiBtw7QgaMOsbmggdOG7kWkgxrB1IHZp4buHYyBnaeG6o20gdGhp4buDdSDEkeG7mSBzYWkgc+G7kSB0cm9uZyBk4buxIGLDoW8uClRow6wgZ2nhuqNpIHF1eeG6v3QgY8OhaSB2acOqYyBuw6B5IGLhurFuZyBjw6FjaCDEkcawYSB0aMOqbSAxIHTDrW5oIHJhbmRvbW5lc3MgbuG7r2EgbMOgICoqU3BsaXQtdmFyaWJhbGUgcmFuZG9taXphdGlvbioqLiBU4bupYyBsw6AgY2hpYSB0w6FjaCBuaMOhbmggY8OieSDEkeG6p3UgdGnDqm4gKHJvb3QpIGTGsGEgdHLDqm4gdmnhu4djIHhlbSB4w6l0IMSRYSBk4bqhbmcgaMOzYSBjw6FjIGJp4bq/biBt4buZdCBjw6FjaCBuZ+G6q3Ugbmhpw6puLiAqKlbDrCB24bqteSByYW5kb21mb3Jlc3QgbMOgIHBoacOqbiBi4bqjbiBj4bqjaSB0aeG6v24gY+G7p2EgdGh14bqtdCB0b8OhbiBCYWdnaW5nKioKIAojIyMgMS4xLjEgw50gdMaw4bufbmcgY+G7p2EgQm9vc3RyYXAKCkJvb3N0cmFwIGzDoCBwaMawxqFuZyBwaMOhcCB0w6FpIGNo4buNbiBt4bqrdSBuZ+G6q3Ugbmhpw6puIHbDoCDEkeG7mWMgbOG6rXAgY8OzIHRyw7luZyBs4bq3cCB04burIHThuq1wIGThu68gbGnhu4d1IHRyYWluIGfhu5FjIHRow6BuaCBuaGnhu4F1IHThuq1wIG3huqt1IGjGoW4uIMSQxqFuIHRodeG6p24gbsOzIGNo4buJIGzDoCB2aeG7h2MgbOG6pXkgbeG6q3Ugbmfhuqt1IG5oacOqbiBjw7MgdHLDuW5nIGzhurdwIG5o4buvbmcgaMOgbmcgY+G7p2EgdOG6rXAgZOG7ryBsaeG7h3UgdHJhaW4uIEtoaSBtw6AgbOG6pXkgbeG6q3UgY8OzIGNobyBwaMOpcCB0csO5bmcgbOG6t3AgdGjDrCBt4buXaSBow6BuZyBjw7MgdGjhu4MgbOG6t3AgbOG6oWkgbmhp4buBdSBs4bqnbiB2w6AgbeG7mXQgc+G7kSBow6BuZyB0aMOsIGLhu4sgduG6r25nIG3hurd0LiBDw6FpIMO9IHTGsOG7n25nIGPhu6dhIG7DsyBsw6AgbXXhu5FuIHThuqFvIHJhIG3hu5l0IHThuq1wIGThu68gbGnhu4d1IG3hu5tpIG3DoCBjw7MgZ2nhu68gxJHGsOG7o2MgbeG7mXQgc+G7kSBuaOG7r25nIHTDrW5oIGNo4bqldCDEkeG6t2MgdHLGsG5nIGPhu6dhIHThuq1wIHRyYWluIChk4buvIGxp4buHdSBn4buRYyBiYW4gxJHhuqd1KSB2w6wgdGjhur8gbcOgIGNow7puZyB0YSBjw7MgdGjhu4MgaHXhuqVuIGx1eeG7h24gMSBtw7QgaMOsbmggZ2nhu5FuZyBuaGF1IGNobyBuaGnhu4F1IHThuq1wIGThu68gbGnhu4d1IMSR4buDIGzhuqV5IHJhIG5o4buvbmcgxJHhurdjIHTDrW5oIHRpw6p1IGJp4buDdSBuaOG6pXQuCgohW10oaW1hZ2VzL2Jvb3RzdHJhcC5QTkcpCgpDw7MgdGjhu4MgaGnhu4N1IHRoZW8gY8OhYyBixrDhu5tjIHNhdToKCkLGsOG7m2MgMTogTOG6pXkgbSDEkeG7kWkgdMaw4bujbmcgbmfhuqt1IG5oacOqbiBjw7MgdHLDuW5nIGzhurdwIHThu6sgdHJvbmcgbiDEkeG7kWkgdMaw4bujbmcg4bufIHThuq1wIHRyYWluIChk4buvIGxp4buHdSBn4buRYykuIG0gdGjDrCBuaOG7jyBoxqFuIGhv4bq3YyBi4bqxbmcgbi4gCgohW10oaW1hZ2VzL3N0ZXAxLnBuZykKCkLGsOG7m2MgMjogSHXhuqVuIGx1eeG7h24gY8OieSBxdXnhur90IMSR4buLbmggdHLDqm4gbmjhu69uZyBt4bqrdSBt4bubaSDEkcaw4bujYyB04bqhbyB0aMOgbmguIEzhurdwIGzhuqFpIG5o4buvbmcgYsaw4bubYyBuw6B5IGJhbyBuaGnDqnUgbOG6p24gdMO5eSB0aMOtY2guIEPDoG5nIG5oaeG7gXUgY8OieSB0aMOsIG3DtCBow6xuaCBjw6BuZyB04buRdC4KCiFbXShpbWFnZXMvc3RlcDIucG5nKQoKQsaw4bubYyAzOiBHaeG6oyBz4butIGjDrG5oIHRyw6puIGzDoCBjw7MgMTAwMCBjw6FpIGPDonksIG3hu5dpIGPDoWkgY8OieSB0aMOsIHPhur0gbGnDqm4gcXVhbiDEkeG6v24gbmjhu69uZyDEkeG6t2MgdMOtbmgga2jDoWMgbmhhdSBj4bunYSBk4buvIGxp4buHdSBzbyB24bubaSAxIGPDoWkgY8OieSBjaG8gdG/DoG4gYuG7mSBk4buvIGxp4buHdSB2w6AgbeG7l2kgY8OhaSBjw6J5IG7DoHkgc+G6vSBjw7Mgc+G7kSDEkWnhu4NtIGvhur90IHRow7pjIGtow6FjIG5oYXUgKHRlcm1pbmFsIG5vZGUpLCBzYXUgxJHDsyB0cmFpbiBt4buXaSBjw6J5IHbDoCBs4bqleSBnacOhIHRy4buLIHRydW5nIGLDrG5oIGPhu6dhIHThu5VuZyB04bqldCBj4bqjIGPDoWMgY8OieSBjaG8gbeG7l2kgY2xhc3MgxJHhu4MgbMOgbSBr4bq/dCBxdeG6oyBjdeG7kWkgY8O5bmcKCiFbXShpbWFnZXMvc3RlcDMucG5nKQoKCiMjIyAxLjEuMiDDnSB0xrDhu59uZyBj4bunYSBTcGxpdC12YXJpYWJsZSByYW5kb21pemF0aW9uIAoKVGjDrCBjw6FpIHRodeG6rXQgdG/DoW4gY8OieSBxdXnhur90IMSR4buLbmggdGjDrCBuw7Mgc+G6vSBi4bqvdCDEkeG6p3UgYuG6sW5nIGPDoWNoIHTDrG0gdG/DoG4gYuG7mSB04bqldCBj4bqjIG5o4buvbmcgYmnhur9uIMSR4bq3YyB0w61uaCB0cm9uZyBib290c3RyYXAgc2FtcGxlIHbDoCBzYXUgxJHDsyBuw7Mgc+G6vSB0w6xtIGPDoWkgYmnhur9uIG7DoG8gbMOgIGJp4bq/biB04buRdCBuaOG6pXQgbcOgIGJp4bq/biBuw6B5IGPDsyB0aOG7gyBkw7luZyDEkeG7gyBjaGlhIHTDoWNoIHJhIG5o4buvbmcgbmjDs20gY8OzIHTDrW5oIMSR4buTbmcgbmjhuqV0IGNhbyBuaOG6pXQuIFThu6ljIGzDoCBraOG6oyBuxINuZyBwaMOibiBiaeG7h3QgbOG7m24gbmjhuqV0LgpUaMOsIHbDrCBjw6FpIG7huqN5IHjhuqN5IHJhIG5oxrAgbmhhdSDhu58gdOG7q25nIGPDonkgdGjDrCBuw7Mgc+G6vSBk4bqrbiDEkeG6v24gc+G7sSBraMO0bmcgbeG6pXkga2jDoWMgYmnhu4d0IG5oYXUgbeG6pXkgduG7gSBj4bqldSB0csO6YyDhu58gbeG7l2kgY8OieSBxdXnhur90IMSR4buLbmggdHJvbmcgcuG7q25nIGPDonkgZOG6q24gxJHhur9uIHPhu7EgZ+G7jWkgbMOgIHRyZWUgY29ycmVsYXRpb24gbmjGsCDEkcOjIG7Ds2kg4bufIHRyw6puLiBWw6wgduG6rXkgdGh14bqtdCB0b8OhbiByYW5kb20gZm9yZXN0IHBo4bqjaSBraOG7kW5nIGNo4bq/IGPDoWkgY2h1eeG7h24gbsOgeSBi4bqxbmcgY8OhY2ggbMOgIGtow7RuZyDEkWkgdMOsbSB0b8OgbiBi4buZIG5o4buvbmcgYmnhur9uIMSR4bq3YyB0w61uaCBtw6AgY2jhu4kgY2jhu41uIHThu6tuZyBj4bulbSBiaeG6v24gbmfhuqt1IG5oacOqbiB0cm9uZyB04buVbmcgc+G7kSBiaeG6v24sIHLhu5NpIMSRaSB0w6xtIHJhIGJp4bq/biB04buRdCBuaOG6pXQgdHJvbmcgY+G7pW0gxJHDsyDEkeG7gyB0aOG7sWMgaGnhu4duIHZp4buHYyBjaGlhIHTDoWNoLgoKIyMjIyBL4bq/dCBo4bujcCBs4bqhaSBj4bqjIDIgw70gdMaw4bufbmcgYmFnZ2luZyBib290c3RyYXAsIFNwbGl0LXZhcmlhYmxlIHJhbmRvbWl6YXRpb24gdGjDrCBn4buNaSBsw6AgcmFuZG9tIGZvcmVzdCAKCiMjIDIuIFRoYW5nIMSRbyBt4bupYyDEkeG7mSB0aOG7gyBoaeG7h24gY+G7p2EgbcO0IGjDrG5oIFJhbmRvbSBGb3Jlc3QKQ29uZnVzaW9uIE1hdHJpeDogQSBicmVha2Rvd24gb2YgcHJlZGljdGlvbnMgaW50byBhIHRhYmxlIHNob3dpbmcgY29ycmVjdCBwcmVkaWN0aW9ucyAodGhlIGRpYWdvbmFsKSBhbmQgdGhlIHR5cGVzIG9mIGluY29ycmVjdCBwcmVkaWN0aW9ucyBtYWRlICh3aGF0IGNsYXNzZXMgaW5jb3JyZWN0IHByZWRpY3Rpb25zIHdlcmUgYXNzaWduZWQpLgoKUHJlY2lzaW9uOiBBIG1lYXN1cmUgb2YgYSBjbGFzc2lmaWVycyBleGFjdG5lc3MuCgpSZWNhbGw6IEEgbWVhc3VyZSBvZiBhIGNsYXNzaWZpZXJzIGNvbXBsZXRlbmVzcwoKRjEgU2NvcmUgKG9yIEYtc2NvcmUpOiBBIHdlaWdodGVkIGF2ZXJhZ2Ugb2YgcHJlY2lzaW9uIGFuZCByZWNhbGwuCgpST0MgQ3VydmVzOiBMaWtlIHByZWNpc2lvbiBhbmQgcmVjYWxsLCBhY2N1cmFjeSBpcyBkaXZpZGVkIGludG8gc2Vuc2l0aXZpdHkgYW5kIHNwZWNpZmljaXR5IGFuZCBtb2RlbHMgY2FuIGJlIGNob3NlbiBiYXNlZCBvbiB0aGUgYmFsYW5jZSB0aHJlc2hvbGRzIG9mIHRoZXNlIHZhbHVlcy4KCgojIyAzLiBQaOG6p24gdGnhu4FuIHjhu60gbMO9IGThu68gbGnhu4d1IHbDoCB0aOG7kW5nIGvDqiB24buBIGPhuqV1IHRyw7pjIGLhuqNuZyBk4buvIGxp4buHdQpH4buNaSBjw6FjIHRoxrAgdmnhu4duIGPhuqduIHRoaeG6v3QgxJHhu4MgbMOgbSB2aeG7h2MKYGBge3J9CiMgbG9hZCB0b29scwpsaWJyYXJ5KHRpZHl2ZXJzZSkgICAgIyBmb3IgaGFuZGxpbmYgZGF0YQpsaWJyYXJ5KGRwbHlyKSAgICAgICAgIyBtYW5pcHVsYXRpbmcgZGF0YQpsaWJyYXJ5KHJlYWR4bCkgICAgICAgIyByZWFkIHhsc3ggZmlsZQpsaWJyYXJ5KHJzYW1wbGUpICAgICAgIyBkYXRhIHNwbGl0dGluZyAKbGlicmFyeShyYW5kb21Gb3Jlc3QpICMgYmFzaWMgaW1wbGVtZW50YXRpb24KbGlicmFyeShyYW5nZXIpICAgICAgICMgYSBmYXN0ZXIgaW1wbGVtZW50YXRpb24gb2YgcmFuZG9tRm9yZXN0CmxpYnJhcnkoY2FyZXQpICAgICAgICAjIGFuIGFnZ3JlZ2F0b3IgcGFja2FnZSBmb3IgcGVyZm9ybWluZyBtYW55IG1hY2hpbmUgbGVhcm5pbmcgbW9kZWxzCmxpYnJhcnkoaDJvKSAgICAgICAgICAjIGFuIGV4dHJlbWVseSBmYXN0IGphdmEtYmFzZWQgcGxhdGZvcm0KbGlicmFyeShnZ3Bsb3QyKSAgICAgICMgZm9yIHZpc3VhbGl6YXRpb24KbGlicmFyeShwUk9DKSAgICAgICAgICMgZm9yIGNhbGN1bGF0ZSBtdWx0aXBsZSBjbGFzcyBBVUMgYW5kIHBsb3QgaXQKbGlicmFyeShyYW5kb21Gb3Jlc3RFeHBsYWluZXIpIApsaWJyYXJ5KGtuaXRyKQpsaWJyYXJ5KGthYmxlRXh0cmEpCmxpYnJhcnkoeGxzeCkKYGBgCgpDaOG7iW5oIGzhuqFpIHF1eSDGsOG7m2MgaGnhu4NuIHRo4buLIHPhu5EgdGhlbyBuZ8O0biBuZ+G7ryBraG9hIGjhu41jCmBgYHtyfQojIHNldCB0byBzY2llbnRpZmljIG5vdGF0aW9uCm9wdGlvbnMoInNjaXBlbiI9MCwgImRpZ2l0cyI9NykKYGBgCgpMb2FkIGThu68gbGnhu4d1IHbDoCBmb3JtYXQgZOG7ryBsaeG7h3UKYGBge3J9CiMgbG9hZCBhbmQgZm9ybWF0IGRhdGEKCmZ1bGwgPSByZWFkLnhsc3goJ2Z1bGwueGxzeCcsIHNoZWV0SW5kZXggPSAxKSAjIHJlYWQgZGF0YSB3aXRoIGxhYmVsIGFuZCBtZXRhIGluZm9ybWF0aW9uCmZ1bGwxID0gZnVsbFstd2hpY2goaXMubmEoZnVsbCkpLF0gIyByZW1vdmUgdW5hdmFpbGFiZWwgY29udGVudApmdWxsMSR1c2VyX2lkID0gYXMuY2hhcmFjdGVyKGZ1bGwxJHVzZXJfaWQpICMgY29udmVydCB1c2VyX2lkIGZyb20gZmFjdG9yIHRvIGNoYXJhY3RlciB0eXBlIG9mIHNhbXBsZSBkYXRhCmNoZWNrID0gcmVhZC5jc3YoJ2ZlYXR1cmVzLmNzdicpIyBsb2FkIHRoZSBkYXRhIHdpdGggbmVjZXNzYXJ5IGZlYXR1cmVzZApjb2xuYW1lcyhjaGVjaylbMl0gPSAndXNlcl9pZCcgIyByZW5hbWUgdGhlIHVzZXIgaWQgdG8gdGhlIGNvbnNlbnN1cyBuYW1lIHdpdGggbmFtZXMgaW4gbGFiZWwgZGF0YQpjaGVjayA9IGNoZWNrWywtMV0KY2hlY2skdXNlcl9pZCA9IGFzLmNoYXJhY3RlcihjaGVjayR1c2VyX2lkKSAjIGNvbnZlcnQgdXNlcl9pZCBmcm9tIGZhY3RvciB0byBjaGFyYWN0ZXIgdHlwZSBvZiBkYXRhYmFzZSBkYXRhCmZ1bGwyID0gaW5uZXJfam9pbihmdWxsMSwgY2hlY2ssIGJ5ID0gJ3VzZXJfaWQnKSAjIGpvaW4gdGhlIHNhbXBsZSBkYXRhIHdpdGggZGF0YWJhc2UgZGF0YSwgaW50ZXJzZWN0aW9uIGpvaW5lZCBieSAndXNlcl9pZCcgCmZ1bGwzID0gZnVsbDJbLXdoaWNoKGZ1bGwyJExhYmVsID09ICdOQScpLCBdICMgcmVtb3ZlIHRoZSBOQSBsYWJlbApmdWxsMyRMYWJlbCA9IGFzLmNoYXJhY3RlcihmdWxsMyRMYWJlbCkKZnVsbDMgPSBmdWxsM1ssLTFdCmBgYAoKWGVtIHF1YSB0aOG7kW5nIGvDqiB24buBIGThu68gbGnhu4d1IApgYGB7cn0KcHJpbnQoZGltKGZ1bGwzKSkKYGBgCkThu68gbGnhu4d1IGhp4buHbiB04bqhaSBjw7MgODIyIGjDoG5nIHbDoCAzMyBj4buZdAoKWGVtIHF1YSAxMCDEkeG7kWkgdMaw4bujbmcgYmFuIMSR4bqndSBjw7Mgc+G7kSBsxrDhu6NuZyBsaWtlIHRyw6puIHThu5VuZyBz4buRIHBvc3QgY2FvIG5o4bqldApgYGB7cn0KdGVuX3Blb3BsZSA9IGZ1bGwzICU+JSBhcnJhbmdlKGRlc2ModG90YWxMaWtlc1Bvc3RzKSkKI2hlYWQodGVuX3Blb3BsZVssIC0xXSwgMTApICAlPiUga2FibGUoKSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9ICJzdHJpcGVkIiwgZm9udF9zaXplID0gOCkKcHJpbnQodGVuX3Blb3BsZVssIC0xXSkKYGBgCgpYZW0gcXVhIGPhuqV1IHRyw7pjIGThu68gbGnhu4d1IHbDoCDEkeG7i25oIGThuqFuZyBj4bunYSB04burbmcgYmnhur9uCmBgYHtyfQpwcmludChzdHIoZnVsbDNbLCAtKDE6NCldKSkKYGBgCgpYZW0gcXVhIHRo4buRbmcga8OqIHbhu4EgbeG7l2kgYmnhur9uIMSR4bq3YyB0csawbmcgY+G7p2EgdOG6rXAgZOG7ryBsaeG7h3UKYGBge3J9CnByaW50KHN1bW1hcnkoZnVsbDNbLC0oMTo2KV0pKQpgYGAKCiMjIyA0LiBU4bqhbyB04bqtcCB0cmFpbiBzZXQgdsOgIHRlc3Qgc2V0IHRyw6puIGThu68gbGnhu4d1IGZ1bGwzCgpUcsaw4bubYyBo4bq/dCBnaWVvIGjhuqF0IGLhurFuZyAxMyDEkeG7gyBzYXUgbsOgeSBt4buXaSBs4bqnbiBjaOG6oXkgY8OzIG3hu5l0IGvhur90IHF14bqjIGdp4buRbmcgbmhhdS4gVsOsIHJhbmRvbSBmb3Jlc3QgY2jhuqF5IGLhurFuZyBib290c3RyYXAgY2hvIG7Dqm4gcGjhuqNpIGdpZW8gaOG6oXQgxJHhu4MgY8OzIHRo4buDIHTDoWkgaGnhu4duIGzhuqFpIGvhur90IHF14bqjIGtoaSBuZ+G6q3Ugbmhpw6puIGNo4buNbiBt4bqrdS4KYGBge3J9CnNldC5zZWVkKDEzKQpgYGAKClBow6JuIGNoaWEgZOG7ryBsaeG7h3UgdGhlbyB04buJIGzhu4cgNzozLCA3IGNobyB0cmFpbiBzZXQgdsOgIDMgY2hvIHRlc3Qgc2V0IAoKYGBge3J9CiMgY3JlYXRlIGEgcmFuZG9tIGluZGV4IGZvciBldmVyeSByb3cgb2YgZGF0YSBmcmFtZQp0cmFpbl9kYXRhX2luZGV4ID0gc2FtcGxlKDE6IG5yb3coZnVsbDMpLCByb3VuZChucm93KGZ1bGwzKSouNykpICMgY3JlYXRlIGEgcmFuZG9tIGluZGV4IGluIDgxOCBvYnNlcnZhdGlvbnMsIHdpdGggdGhlIDc6MyByYXRpbwoKIyBjcmVhdGUgdHJhaW5uaW5nIGRhdGEgYW5kIGZvcm1hdAp0cmFpbl9kYXRhID0gZnVsbDNbdHJhaW5fZGF0YV9pbmRleCxdCnRyYWluX2RhdGEgPSB0cmFpbl9kYXRhICU+JSBzZWxlY3QoLXVzZXJfaWQsIC1OYW1lLCAtRGVzY3JpcHRpb24sIC10b3RhbExpa2VzUG9zdHMsIC10b3RhbENvbW1lbnRzUG9zdHMsIC1pbWFnZXNfY291bnRfcG9zdCkKdHJhaW5fZGF0YSRMYWJlbCA9IGFzLmNoYXJhY3Rlcih0cmFpbl9kYXRhJExhYmVsKQp0cmFpbl9kYXRhJExhYmVsID0gYXMuZmFjdG9yKHRyYWluX2RhdGEkTGFiZWwpCgojIGNyZWF0ZSB0ZXN0aW5nIGRhdGEgYW5kIGZvcm1hdAp0ZXN0X2RhdGEgPSBmdWxsM1stdHJhaW5fZGF0YV9pbmRleCxdCnRlc3RfZGF0YSA9IHRlc3RfZGF0YSAlPiUgc2VsZWN0KC11c2VyX2lkLCAtTmFtZSwgLURlc2NyaXB0aW9uLCAtdG90YWxMaWtlc1Bvc3RzLCAtdG90YWxDb21tZW50c1Bvc3RzLCAtaW1hZ2VzX2NvdW50X3Bvc3QpCnRlc3RfZGF0YSRMYWJlbCA9IGFzLmNoYXJhY3Rlcih0ZXN0X2RhdGEkTGFiZWwpCnRlc3RfZGF0YSRMYWJlbCA9IGFzLmZhY3Rvcih0ZXN0X2RhdGEkTGFiZWwpCmBgYAoKWGVtIGzhuqFpIGvDrWNoIHRoxrDhu5tjIGPhu6dhIG3hu5dpIHThuq1wCgpUcmFpbiBzZXQKYGBge3J9CnByaW50KGRpbSh0cmFpbl9kYXRhKSkKYGBgCgpUZXN0IHNldApgYGB7cn0KcHJpbnQoZGltKHRlc3RfZGF0YSkpCmBgYAoKIyMjIDUuIFhlbSBz4buRIGPDonkg4bufIGtob+G6o25nIGJhbyBuaGnDqnUgdGjDrCDEkeG7mSBzYWkgc+G7kSBj4bunYSBtw7QgaMOsbmggZ2nhuqNtIHh14buRbmcgbmhp4buBdSBuaOG6pXQuCgpC4bufaSB2w6wgbeG7l2kgY8OieSBjb24gdHJvbmcgcmFuZG9tIGZvcmVzdCB0aMOsIMSRxrDhu6NjIGh14bqlbiBsdXnhu4duIGThu7FhIHRyw6puIG3huqt1IMSRxrDhu6NjIHThuqFvIGLhu59pIHBoxrDGoW5nIHBow6FwIGJvb3RzdHJhcCAodMOhaSBjaOG7jW4gbeG6q3Ugbmjhu48gaMahbiB0csOqbiB04bqtcCBk4buvIGxp4buHdSB0cmFpbiBkYXRhIGfhu5FjKSBjaG8gbsOqbiBt4buZdCBz4buRIMSR4buRaSB0xrDhu6NuZyB0cm9uZyBt4bqrdSDEkcaw4bujYyB04bqhbyBz4bq9IGLhu4sgdHLDuW5nIGzhurdwIHbDoCBt4buZdCBz4buRIMSR4buRaSB0xrDhu6NuZyBz4bq9IGLhu4sgbG/huqFpIHJhLiBUaMOsIHThuq1wIG5o4buvbmcgY8OhaSDEkeG7kWkgdMaw4bujbmcgYuG7iyBsb+G6oWkgcmEgc+G6vSDEkcaw4bujYyBn4buNaSBsw6AgT1VUIE9GIEJBRyBzYW1wbGVzIHbDoCBz4bq9IMSRxrDhu6NjIGTDuW5nIG5oxrAgbeG7mXQgdOG6rXAgdmFsaWRhdGlvbiBzZXQuCgpC4bufaSB2w6wgY8OhaSB04bqtcCBPT0IgbsOgeSBraMO0bmcgxJHGsOG7o2MgZMO5bmcgxJHhu4MgaHXhuqVuIGx1eeG7h24gbmjhu69uZyBjw6FpIGPDonksIGNobyBuw6puIG7DsyBz4bq9IMSRxrDhu6NjIGTDuW5nIMSR4buDIMSRw6FuaCBnacOhIGto4bqjIG7Eg25nIHRo4buDIGhp4buHbiBj4bunYSBtw7QgaMOsbmggY8OieSB0csOqbiBk4buvIGxp4buHdSBjaMawYSDEkcaw4bujYyB0aeG6v3AgY+G6rW4gxJHhur9uLiAgCgpEw7luZyB0aGFuZyDEkW8gT09CIGVycm9yIHJhdGUgxJHhu4MgxJHDoW5oIGdpw6EgeGVtIHPhu5EgY8OieSBj4bqnbiBjaGlhIHJhIGzDoCBiYW8gbmhpw6p1IMSR4buDIGdp4bqjbSDEkeG7mSBzYWkgc+G7kSBj4bunYSBtw7QgaMOsbmggeHXhu5FuZyBuaOG7jyBuaOG6pXQuClRow6wgxJHDonkgbMOgIGVycm9yIHJhdGUgbcOgIMSRxrDhu6NjIHTDrW5oIHThu6sgbmjhu69uZyBzYW1wbGUgbcOgIGtow7RuZyDEkcaw4bujYyBs4buxYSBjaOG7jW4gcmEga2hpIHRo4buxYyBoaeG7h24gdmnhu4djIHTDoWkgY2jhu41uIG3huqt1KGJvb3RzdHJhcCwgcmFuZG9tIHNhbXBsaW5nKQoKU2VhcmNoIHRyb25nIGtob+G6o25nIDIwMDAgY8OieS4KCgpgYGB7cn0KIyBmaW5kIHRoZSBudW1iZXIgb2YgdHJlZSAKc2V0LnNlZWQoMTMpCiAgIyBkZWZhdXRsIHJmIG1vZGVsIHdpdGggMjAwMCB0cmVlcwogIG0xID0gcmFuZG9tRm9yZXN0KAogICAgZm9ybXVsYSA9IExhYmVsfi4gLAogICAgZGF0YSA9IHRyYWluX2RhdGEsCiAgICBudHJlZSA9IDIwMDAKICApCgogICMgbnVtYmVyIG9mIHRyZWUgd2l0aCB0aGUgZXJyb3IgcmF0ZSAsIHBsb3R0aW5nCiAgbTEgIyAyMDAwIHRyZWVzCiAgcGxvdChtMSwgbWFpbiA9ICdlcnJvciByYXRlIHdpdGggMjAwMCB0cmVlcycpCiAgbGVnZW5kKCJ0b3ByaWdodCIsIGNvbG5hbWVzKG0xJGVyci5yYXRlKSxjb2w9MTo0LGNleD0wLjgsZmlsbD0xOjQpCiAgCiAgIyBjb252ZXJ0IGZvcm0gbWF0cml4IHRvIGRhdGEgZnJhbWUKICBlcnIuZGZfMjAwMCA9IG0xJGVyci5yYXRlICU+JSBhcy5kYXRhLmZyYW1lKCkKCiAgI2ZpbmQgdGhlIG51bWJlciBvZiB0cmVlIHRoYXQgY29ycmVzcG9uZGVudCB0byBtaW4gZXJyb3IgcmF0ZQogIHdoaWNoLm1pbihlcnIuZGZfMjAwMCRPT0IpCiAgCmBgYAoKCk5ow6xuIHbDoG8gaMOsbmggdsOgIGvhur90IHF14bqjIHRy4bqjIHbhu4EgdGEgdGjhuqV5IG3hu6ljIGVycm9yIHJhdGUgbmjhu48gbmjhuqV0IOG7qW5nIHbhu5tpIG3hu6ljIGzDoCAxODEgY8OieS4KCiMjIyA3LiBDaOG6oXkgdGjhu60gbW9kZWwgduG7m2kgY8OhYyB0aGFtIHPhu5Eg4bufIG3hu6ljIG3hurdjIMSR4buLbmggdsOgIHPhu5EgY8OieSBsw6AgMTgxIGPDonkuCmBgYHtyfQojIFNJTVBMRSBNT0RFTAojIG1vZGVsIGRhdGEKc2V0LnNlZWQoMTMpCnJmID0gcmFuZG9tRm9yZXN0KExhYmVsIH4gLiAsIGRhdGEgPSB0cmFpbl9kYXRhLCBpbXBvcnRhbmNlID0gVCwgbnRyZWUgPSAxODEgKQoKIyBjb21wYXJlIHRoZSBwcmVkaWN0ZWQgbGFiZWwgd2l0aCB0aGUgYWN0dWFsIGxhYmVsCnJlc3VsdF9yZiA9IHByZWRpY3QocmYsIG5ld2RhdGEgPSB0ZXN0X2RhdGEpCmNvbmZ1c2lvbk1hdHJpeChyZXN1bHRfcmYsIHRlc3RfZGF0YSRMYWJlbCwgbW9kZSA9ICdldmVyeXRoaW5nJykKCiMgY2FsY3VsYXRlIHRoZSBhdWMKCmFjdHVhbCA9IGFzLm51bWVyaWModGVzdF9kYXRhJExhYmVsICkKcHJlZGljdGVkID0gYXMubnVtZXJpYyhyZXN1bHRfcmYpCnJlc3VsdF9yb2MgPSBtdWx0aWNsYXNzLnJvYyhhY3R1YWwsIHByZWRpY3RlZCkKcmVzdWx0X3JvYwphdWMocmVzdWx0X3JvYykKYGBgCgpYZW0gZXJyb3IgcmF0ZSB0aGF5IMSR4buVaSB0aGVvIHPhu5EgY8OieSB0xrDGoW5nIOG7qW5nCmBgYHtyfQojIHBsb3QgdGhlIGVycm9yIHJhdGUgYW5kIG51bWJlciBvZiB0cmVlCgpwbG90KHJmLCBtYWluID0gJ0xlYXJuaW5nIGN1cnZlJykKbGVnZW5kKCJ0b3ByaWdodCIsIGNvbG5hbWVzKHJmJGVyci5yYXRlKSxjb2w9MTo0LGNleD0wLjgsZmlsbD0xOjQpCmBgYAoKWGVtIGPDoWMgYmnhur9uIHF1YW4gdHLhu41uZyDEkcOzbmcgZ8OzcCB2w6BvIG3DtCBow6xuaCBk4buxIMSRb8OhbgpgYGB7cn0KdmFySW1wUGxvdChyZiwgbWFpbiA9ICdJbXBvcnRhbnQgdmFyaWFibGUnLCBjZXggPSAuNykKYGBgCgpUcsOqbiBow6xuaCB0csOqbiB0YSBjw7MgdGjhu4MgdGjhuqV5IDUgYmnhur9uIHF1YW4gdHLhu41uZyDEkcOzbmcgZ8OzcCB2w6BvIG3DtCBow6xuaCBsw6AgOgpgYGB7cn0KcmYgJT4lIGltcG9ydGFudF92YXJpYWJsZXMoaz01LHRpZXNfYWN0aW9uID0gImRyYXciKQpgYGAKCgoKWGVtIHRow6ptIGPDoWMgxJHhu5FpIHTGsOG7o25nIG7DoG8gYuG7iyBwaMOibiBsb+G6oWkgc2FpCmBgYHtyfQp3aGljaChyZXN1bHRfcmYgIT0gdGVzdF9kYXRhJExhYmVsKQplcnJvciA9IGZ1bGwzWy10cmFpbl9kYXRhX2luZGV4LF1bd2hpY2gocmVzdWx0X3JmICE9IHRlc3RfZGF0YSRMYWJlbCksIF0KZXJyb3IkcHJlZGljdF9sYWJlbCA9IHJlc3VsdF9yZlt3aGljaChyZXN1bHRfcmYgIT0gdGVzdF9kYXRhJExhYmVsKV0KY29tcGFyZSA9IGVycm9yICU+JSBzZWxlY3QodXNlcl9pZCwgTmFtZSwgRGVzY3JpcHRpb24sIExhYmVsLCBwcmVkaWN0X2xhYmVsKQoKIyBrYWJsZShjb21wYXJlKSAlPiUga2FibGVfc3R5bGluZyhib290c3RyYXBfb3B0aW9ucyA9ICJzdHJpcGVkIiwgZm9udF9zaXplID0gOCkKIyBrYWJsZShlcnJvcikgJT4lIGthYmxlX3N0eWxpbmcoYm9vdHN0cmFwX29wdGlvbnMgPSAic3RyaXBlZCIsIGZvbnRfc2l6ZSA9IDgpCgpwcmludChjb21wYXJlKQpwcmludChlcnJvclssIC0xXSkKCmBgYAoKCgoKIyMjIDguIEvhur90IGx14bqtbiAKCiFbXShpbWFnZXMvZmlyc3RfbW9kZWwucG5nKQoKCgrhu54gxJHDonkgdGEgY8OzIHRo4buDIHRo4bqleSBsw6AgxJHhu5kgY2jDrW5oIHjDoWMgY+G7p2EgbcO0IGjDrG5oIHBow6JuIGxv4bqhaSBjaG8gMiBjbGFzcyBsw6AgRkFOIGzDoCBy4bqldCBjYW8uCgpDbGFzcyBGQU4gY8OzIMSR4buZIGNow61uaCB4w6FjIFByZWNpc2lvbiA6IDAuOTg2IHbDoCBt4bupYyDEkeG7mSBraMO0bmcgYuG7jyBzw7N0IFJlY2FsbDogbMOgIDA5ODgKCkNsYXNzIEtPTCBjw7MgxJHhu5kgY2jDrW5oIHjDoWMgUHJlY2lzaW9uIDogMC42NjY3IHbDoCBt4bupYyDEkeG7mSBraMO0bmcgYuG7jyBzw7N0IFJlY2FsbDogbMOgIDAuNzEKCkPDsm4geeG6v3UgbmjhuqV0IGzDoCBjbGFzcyBUT0wgduG7m2kgxJHhu5kgY2jDrW5oIHjDoWMgbMOgIDAuNTcxIG5oxrBuZyBt4bupYyDEkeG7mSBraMO0bmcgYuG7jyBzw7N0IGNo4buJIGPDsyAwLjU3CgpDaMOtbmggdsOsIHRo4bq/IMSR4buZIGNow61uaCB4w6FjIEFjY3VyYWN5IDogMC45NTM0IGzDoCB2w7QgbmdoxKlhLCBraMO0bmcgcGjhuqNpIHRoYW5nIMSRbyBjaMOtbmggeMOhYywgbsOzIMSRw6MgYuG7iyDEkeG6qXkgY2FvIGzDqm4gZG8gc+G7sSBt4bqldCBjw6JuIGLhurFuZyBnaeG7ryB04buJIGzhu4cgY8OhYyBjbGFzcy4gVHJvbmcgxJHDsyBGQU4gdsOgIEtPTCBjaGnhur9tIGfhuqduIDk3IHBo4bqnbiB0csSDbSAobcOgIDIgY2xhc3MgbsOgeSBs4bqhaSBwaMOibiBsb+G6oWkgxJHDum5nIG5o4bqldCkuIChYZW0gcGjhu6UgbOG7pWMg4bufIGTGsOG7m2kpCgpgYGB7cn0KcHJpbnQocHJvcC50YWJsZSh0YWJsZShmdWxsMyRMYWJlbCkpKQpgYGAKIApOaMOsbiB2w6BvIGNo4buJIHPhu5EgTm8gSW5mb3JtYXRpb24gUmF0ZSBjaMO6bmcgdGEgdGjhuqV5LCBjaOG7jW4gbmfhuqt1IG5oacOqbiB0aMOsIMSR4buZIGNow61uaCB4w6FjIHRyb25nIDEwMCBs4bqnbiBjaOG7jW4gOTAgcGjhuqduIHRyxINtIHRyb25nIGtoaSDEkcOzIG3DtCBow6xuaCBjaOG7iSB0aOG7gyBoaeG7h24gc+G7sSBj4bqjaSB0aGnhu4duIMSRxrDhu6NjIDUgcGjhuqduIHRyxINtLiBUaMOsIG3DtCBow6xuaCB0aOG6vyBuw6B5IGPDsm4ga8OpbQoKQXJlYSB1bmRlciBST0MgY3VydmUgKEFVQykgOiAwLjg1MyBwaOG6o24gw6FuaCDEkcaw4bujYyBwaOG6p24gbsOgbyBz4buxIHRoaeG6v3UgY2jDrW5oIHjDoWMgY+G7p2EgbcO0IGjDrG5oIHbhu5tpIHPhu7EgbeG6pXQgY8OibiBi4bqxbmcgdOG7iSBs4buHIGdp4buvYSBjw6FjIGNsYXNzIEtPTCwgVE9MIHbDoCBGQU4KCk3DoCB2w6wgc2FvIGzhuqFpIGPDsyBz4buxIHBow6JuIGxv4bqhaSBzYWkgbMOgIGRvIGtow7RuZyBjw7Mgbmhp4buBdSBk4buvIGxp4buHdSDEkeG7gyBjaG8gbcOheSBo4buNYyB24buBIFRPTCB2w6AgS09MLgoKCgojIyMgOS4gU+G7sSBt4bqldCBjw6JuIGLhurFuZyB04buJIGzhu4cgZ2nhu69hIGPDoWMgbmjDs20gTGFiZWwKCgpYZW0gbOG6oWkgc+G7sSB04buJIGzhu4cgdsOgIHBow6JuIGLhu5EgY+G7p2EgbmjDo24gZMOhbgpgYGB7cn0KdW5pcXVlKGZ1bGwzJExhYmVsKQpwcmludCh0YWJsZShmdWxsMyRMYWJlbCkpCmBgYAoKUGjDom4gcGjhu5FpIGJp4buDdSBkaeG7hW4KYGBge3J9CmdncGxvdChkYXRhID0gZnVsbDMsCiAgICAgICBhZXMoTGFiZWwpKSArCiAgZ2VvbV9iYXIoYWVzKGZpbGwgPSBMYWJlbCkpCmBgYAoKTmjDrG4gdsOgbyB0csOqbiBow6xuaCB0YSBjw7MgdGjhu4MgdGjhuqV5IHPhu7EgbeG6pXQgY8OibiBi4bqxbmcgY+G7p2EgbmjDo24gRkFOIHNvIHbhu5tpIGPDoWMgbmjDs20gY8OybiBs4bqhaSBsw6AgS09MIHbDoCBUT0wKVOG7iSBs4buHIHRo4buxYyB04bq/IDMgbmjDs20gY2hp4bq/bSBsw6AKYGBge3J9CnByaW50KHByb3AudGFibGUodGFibGUoZnVsbDMkTGFiZWwpKSkKYGBgCgoKIyMjIDEwLiBHaeG6o2kgcXV54bq/dCBz4buxIG3huqV0ICBjw6JuIGLhurFuZyBnaeG7r2EgY8OhYyBjbGFzcyBLT0wsIFRPTCBzbyB24bubaSBGQU4gdsOgIGPhuqNpIHRoaeG7h24gbcO0IGjDrG5oLgoKQuG7n2kgdsOsIG3DtCBow6xuaCBuw7MgcXXDoSBjaMOqbmggbOG7h2NoIGdp4buvYSBjw6FjIGNsYXNzIG5oxrAgxJHDoyBuw7NpIOG7nyBt4bulYyA5LiBNw6AgecOqdSBj4bqndSBsw6AgY+G6p24gbmhp4buBdSBk4buvIGxp4buHdSBjaG8gbsOqbiBwaMawxqFuZyBwaMOhcCBnaeG6o2kgcXV54bq/dCB24bqlbiDEkeG7gSBuw6B5IGzDoCBkw7luZyBwaMawxqFuZyBwaMOhcCAqKk92ZXJzYW1wbGluZyoqLiAKCk3DoCBj4bulIHRo4buDIGzDoCBkw7luZyBrxKkgdGh14bqtdCAqKlNNT1RFOiBTeW50aGV0aWMgTWlub3JpdHkgT3Zlci1zYW1wbGluZyBUZWNobmlxdWUqKi4gS+G7uSB0aHXhuq10IG7DoHkgZMO5bmcgxJHhu4MgdOG6oW8gcmEgbmjhu69uZyBmYWtlIHVzZXIgdGh14buZYyBjw6FjIG5ow7NtIGNoaeG6v20gdOG7iSBs4buHIHRo4bqlcCBuaMawIEtPTCB2w6AgVE9MCgrEkOG7gyBkaeG7hW4gZ2nhuqNpIHbhuqVuIMSR4buBIG7DoHkgdGjDrCB4aW4gZMO5bmcgMSBzbWFsbCBkYXRhIHNldCBy4bqldCBu4buVaSB0aeG6v25nIGzDoCBJUklTIMSR4buDIGRp4buFbiBnaeG6o2kuIElyaXMgbMOgIHThuq1wIGThu68gbGnhu4d1IGfhu5NtIGPDoWMgbG/huqFpIGhvYSBsw6AgU2V0b3NhLCB2ZXJzaWNvbG9yIHbDoCB2aXJnaW5pY2EgdsOgIGPDoWMgxJHhurdjIHTDrW5oIGPhu6dhIGPDoWMgbG/huqFpIGhvYSBuaMawIGzDoCDEkeG7mSBkw6BpIGPhu6dhIMSRw6BpIGhvYSAuLi4uCgoKYGBge3J9CnNtb3RlID0gaXJpc1tjKDIsIDksIDE2LCAyMywgNTE6NjMpLAogICAgICAgICBjKDEsIDIsIDUpXQpzbW90ZSRTcGVjaWVzID0gYXMuY2hhcmFjdGVyKHNtb3RlJFNwZWNpZXMpCnByaW50KHNtb3RlKQpgYGAKCmBgYHtyfQp0YWJsZShzbW90ZSRTcGVjaWVzKQpgYGAKVGEgY8OzIHRo4buDIHRo4bqleSBsb+G6oWkgaG9hIHRodeG7mWMgbmjDs20gc2V0b3NhIGNoaeG6v20gc+G7kSBsxrDhu6NuZyA0LCAwLjIzIHBo4bqnbiB0csSDbSwgcuG6pXQgbMOgIG3huqV0IGPDom4gYuG6sW5nIHNvIHbhu5tpIHZlcnNpY29sb3IKCiFbXShpbWFnZXMvY2xhc3NfaW1iYWxhbmNlLnBuZykKIApOaMawIGjDrG5oIG1pbmggaOG7jWEg4bufIHRyw6puLCBsb+G6oWkgaG9hIHNldG9zYSwgdsOybmcgdHLDsm4gbcOgIMSR4buPIGzDoCBuaMOzbSB0aGnhu4N1IHPhu5EgKE1pbm9yaXR5KSBt4bqldCBjw6JuIGLhurFuZyBzbyB24bubaSBuaMOzbSBtw6B1IHhhbmggdmVyc2ljb2xvciAoTWFqb3JpdHkgY2xhc3MpLiDEkOG7gyBtw7QgaMOsbmggZOG7sSBiw6FvIMSRw7puZyBoxqFuLCB0YSBz4bq9IHThuqFvIHRow6ptIG5o4buvbmcgxJFp4buDbSBtw6B1IMSR4buPIHNhbyBjaG8gY8OibiBi4bqxbmcgduG7m2kgxJFp4buDbSBtw6B1IHhhbmguCgpUaMOsIHRodeG6rXQgdG/DoW4gU01PVEUgc+G6vSB24bq9IGPDoWMgxJHGsOG7nW5nIG7hu5FpIGPDoWMgxJFp4buDbSDEkcOzIGzhuqFpIHbhu5tpIG5oYXUgbmjGsCBow6xuaCBkxrDhu5tpCgohW10oaW1hZ2VzL3Ntb3RlX2xpbmUucG5nKQoKVsOsIHNhbyB04bqhbyDEkcaw4bujYyBjw6FjIMSRxrDhu51uZyBu4buRaSBuaGF1IHRow6wgeGluIGdp4bqjaSB0aMOtY2ggbmjGsCBzYXU6CgpUaHXhuq10IHRvw6FuIHPhur0gxJFpIHTDrG0gbmjDs20gY2hp4bq/bSB04buJIGzhu4cgdGjhuqVwIG5o4bqldCwgc2F1IMSRw7MgxJHDsyBuw7Mgc+G6vSBjaOG7jW4gbOG6pXkgMSDEkWnhu4NtIGfhu5FjIG3DoCDEkWnhu4NtIMSRw7MgZ+G6pW4gbmjhuqV0IHbhu5tpIHThuqV0IGPhuqMgY8OhYyDEkWnhu4NtIGPDsm4gbOG6oWkuIFNhdSDEkcOzIGNow7puZyB0YSBz4bq9IGNo4buNbiBiw6FuIGvDrW5oIHbDuW5nIGNow7puZyB0YSBtdeG7kW4gbOG6pXkgY8OhYyDEkWnhu4NtIGThu68gbGnhu4d1IGLhurFuZyBjw6FjaCBjaG8gYmnhur90IHPhu5EgxJFp4buDbSBn4bqnbiBuaOG6pXQgY8OzIHRo4buDIGPDsyBzbyB24bubaSDEkWnhu4NtIGJhbiDEkeG6p3UuIFNhdSDEkcOzIG7hu5FpIGNow7puZyBs4bqhaSB24bubaSBuaGF1LgoKQ+G7pSB0aOG7gyBuaMawIGjDrG5oIGTGsOG7m2ksIGNow7puZyB0YSBtdeG7kW4gY8OzIG5o4buvbmcgxJFp4buDbSBk4buvIGxp4buHdSBnaeG6oyBu4bqxbSB0cm9uZyBiw6FuIGvDrW5oIHThu6sgxJFp4buDbSBn4buRYyDEkeG6v24gMSDEkWnhu4NtIGfhuqduIG5o4bqldCBzbyB24bubaSBuw7MKCiFbXShpbWFnZXMvc21vdGVfazEucG5nKQoKaG/hurdjIGzDoCB04burIMSRaeG7g20gZ+G7kWMgxJHhur9uIDIgxJFp4buDbSBn4bqnbiBuaOG6pXQgc28gduG7m2kgbsOzCgohW10oaW1hZ2VzL3Ntb3RlX2syLnBuZykKCgpWw6AgdOG6oW8gY8OhYyDEkWnhu4NtIGThu68gbGnhu4d1IHRyw6puIGPDoWMgxJHGsOG7nW5nIG7DoHkgbmjGsCBow6xuaCBkxrDhu5tpCgohW10oaW1hZ2VzL3BvaW50X3Ntb3RlLnBuZykKClNhdSDEkcOieSBsw6AgY29kZSDEkeG7gyB04bqhbyB0aHXhuq10IHRvw6FuCgpgYGB7ciwgZXZhbCA9IEYsIGVjaG8gPSBGfQpzeW50aGVzaXMgPSBmdW5jdGlvbiAoWCwgdGFyZ2V0LCBLID0gNSwgZHVwX3NpemUgPSAwKSAKewogICAgbmNEID0gbmNvbChYKQogICAgbl90YXJnZXQgPSB0YWJsZSh0YXJnZXQpCiAgICBjbGFzc1AgPSBuYW1lcyh3aGljaC5taW4obl90YXJnZXQpKQogICAgUF9zZXQgPSBzdWJzZXQoWCwgdGFyZ2V0ID09IG5hbWVzKHdoaWNoLm1pbihuX3RhcmdldCkpKVtzYW1wbGUobWluKG5fdGFyZ2V0KSksIAogICAgICAgIF0KICAgIE5fc2V0ID0gc3Vic2V0KFgsIHRhcmdldCAhPSBuYW1lcyh3aGljaC5taW4obl90YXJnZXQpKSkKICAgIFBfY2xhc3MgPSByZXAobmFtZXMod2hpY2gubWluKG5fdGFyZ2V0KSksIG5yb3coUF9zZXQpKQogICAgTl9jbGFzcyA9IHRhcmdldFt0YXJnZXQgIT0gbmFtZXMod2hpY2gubWluKG5fdGFyZ2V0KSldCiAgICBzaXplUCA9IG5yb3coUF9zZXQpCiAgICBzaXplTiA9IG5yb3coTl9zZXQpCiAgICBrbmVhciA9IGtuZWFyZXN0KFBfc2V0LCBQX3NldCwgSykKICAgIHN1bV9kdXAgPSBuX2R1cF9tYXgoc2l6ZVAgKyBzaXplTiwgc2l6ZVAsIHNpemVOLCBkdXBfc2l6ZSkKICAgIHN5bl9kYXQgPSBOVUxMCiAgICBmb3IgKGkgaW4gMTpzaXplUCkgewogICAgICAgIGlmIChpcy5tYXRyaXgoa25lYXIpKSB7CiAgICAgICAgICAgIHBhaXJfaWR4ID0ga25lYXJbaSwgY2VpbGluZyhydW5pZihzdW1fZHVwKSAqIEspXQogICAgICAgIH0KICAgICAgICBlbHNlIHsKICAgICAgICAgICAgcGFpcl9pZHggPSByZXAoa25lYXJbaV0sIHN1bV9kdXApCiAgICAgICAgfQogICAgICAgIGcgPSBydW5pZihzdW1fZHVwKQogICAgICAgIFBfaSA9IG1hdHJpeCh1bmxpc3QoUF9zZXRbaSwgXSksIHN1bV9kdXAsIG5jRCwgYnlyb3cgPSBUUlVFKQogICAgICAgIFFfaSA9IGFzLm1hdHJpeChQX3NldFtwYWlyX2lkeCwgXSkKICAgICAgICBzeW5faSA9IFBfaSArIGcgKiAoUV9pIC0gUF9pKQogICAgICAgIHN5bl9kYXQgPSByYmluZChzeW5fZGF0LCBzeW5faSkKICAgIH0KICAgIFBfc2V0WywgbmNEICsgMV0gPSBQX2NsYXNzCiAgICBjb2xuYW1lcyhQX3NldCkgPSBjKGNvbG5hbWVzKFgpLCAiY2xhc3MiKQogICAgTl9zZXRbLCBuY0QgKyAxXSA9IE5fY2xhc3MKICAgIGNvbG5hbWVzKE5fc2V0KSA9IGMoY29sbmFtZXMoWCksICJjbGFzcyIpCiAgICByb3duYW1lcyhzeW5fZGF0KSA9IE5VTEwKICAgIHN5bl9kYXQgPSBkYXRhLmZyYW1lKHN5bl9kYXQpCiAgICBzeW5fZGF0WywgbmNEICsgMV0gPSByZXAobmFtZXMod2hpY2gubWluKG5fdGFyZ2V0KSksIG5yb3coc3luX2RhdCkpCiAgICBjb2xuYW1lcyhzeW5fZGF0KSA9IGMoY29sbmFtZXMoWCksICJjbGFzcyIpCiAgICBOZXdEID0gcmJpbmQoUF9zZXQsIHN5bl9kYXQsIE5fc2V0KQogICAgcm93bmFtZXMoTmV3RCkgPSBOVUxMCiAgICBEX3Jlc3VsdCA9IGxpc3QoZGF0YSA9IE5ld0QsIHN5bl9kYXRhID0gc3luX2RhdCwgb3JpZ19OID0gTl9zZXQsIAogICAgICAgIG9yaWdfUCA9IFBfc2V0LCBLID0gSywgS19hbGwgPSBOVUxMLCBkdXBfc2l6ZSA9IHN1bV9kdXAsIAogICAgICAgIG91dGNhc3QgPSBOVUxMLCBlcHMgPSBOVUxMLCBtZXRob2QgPSAiU01PVEUiKQogICAgY2xhc3MoRF9yZXN1bHQpID0gImdlbl9kYXRhIgogICAgcmV0dXJuKERfcmVzdWx0KQp9CmBgYAoKVGjDrCBjw7MgdGjhu4Mgbmjhuq1uIHRo4bqleSDhu58gdHLDqm4gY8OhYyBiaeG6v24gxJHhuqd1IHbDoG8gKipYKiogY2jDrW5oIGzDoCBjw6FjIG1hIHRy4bqtbiBjaOG7qWEgY8OhYyBiaeG6v24gxJHhurdjIHTDrW5oIOG7nyBow6BuZyBj4buZdCwgKip0YXJnZXQqKiBjaMOtbmggbMOgIG5ow6NuIGPhu6dhIGPDoWMgxJHhu5FpIHTGsOG7o25nLCAqKksqKiBjaMOtbmggbMOgIHPhu5EgxJFp4buDbSBn4bqnbiBuaOG6pXQgc28gduG7m2kgxJFp4buDbSBn4buRYy4gKipkdXBfc2l6ZSoqIGNow61uaCBsw6Agc+G7kSDEkWnhu4NtIG3hu5tpIGPhuqduIMSRxrDhu6NjIHThuqFvIGLhurFuZyB04buVbmcgc+G7kSDEkWnhu4NtIGfhu5FjIGPhu5luZyB24bubaSBjw6FjIMSRaeG7g20gZ+G6p24gbsOzIG5o4bqldCBuaMOibiB24bubaSBo4buHIHPhu5EgKipkdXBfc2l6ZSoqCgpTYXUgxJHDonkgc+G6vSBjaOG6oXkgdOG6oW8gZOG7ryBsaeG7h3UgZ2nhuqMgY+G7p2EgY8OhYyBuaMOzbSBi4buLIG3huqV0IGPDom4gYuG6sW5nIGLhurFuZyB0aMawIHZp4buHbiBzbW90ZWZhbWlseQoKYGBge3IsIGV2YWwgPSBGLCBlY2hvID0gVH0KbGlicmFyeShzbW90ZWZhbWlseSkKCmRhdCA9IGZ1bGwzICU+JSBzZWxlY3QoLXVzZXJfaWQsIC1OYW1lLCAtRGVzY3JpcHRpb24sIC1MYWJlbCkKCnNldC5zZWVkKDEzKQojIGNyZWF0ZSBhIFRPTCBzeW50aAp0b2xfYmFsID0gU01PVEUoZGF0LAogICAgICAgICAgICAgICAgYXMubnVtZXJpYyhhcy5mYWN0b3IoZnVsbDMkTGFiZWwpKSwKICAgICAgICAgICAgICAgIEsgPSAxMCwKICAgICAgICAgICAgICAgIGR1cF9zaXplID0gMTEpCnRvbF9zeW50aCA9IHRvbF9iYWwkc3luX2RhdGEKCiMgYWRkIHRoZSBsYWJlbCBmb3IgVE9MIGNsYXNzCnRvbF9zeW50aCR1c2VyX2lkID0gJ2Zha2UgaWQnCnRvbF9zeW50aCROYW1lID0gJ2Zha2UgbmFtZScKdG9sX3N5bnRoJERlc2NyaXB0aW9uID0gJ2Zha2UgZGVzY3JpcHRpb24nCnRvbF9zeW50aCRMYWJlbCA9ICdUT0wnCnRvbF9zeW50aCA9IHRvbF9zeW50aFssIC0yOV0KCgojIGNyZWF0ZSBhIEtPTCBzeW50aApkYXQyID0gdG9sX2JhbCRkYXRhCgprb2xfYmFsID0gU01PVEUoZGF0MlssIC0yOV0sCiAgICAgICAgICAgICAgICBkYXQyJGNsYXNzLAogICAgICAgICAgICAgICAgSyA9IDEwLAogICAgICAgICAgICAgICAgZHVwX3NpemUgPSA5ICkKCmtvbF9zeW50aCA9IGtvbF9iYWwkc3luX2RhdGEKCiMgYWRkIHRoZSBsYWJlbCBmb3IgS09MIGNsYXNzCmtvbF9zeW50aCR1c2VyX2lkID0gJ2Zha2UgaWQnCmtvbF9zeW50aCROYW1lID0gJ2Zha2UgbmFtZScKa29sX3N5bnRoJERlc2NyaXB0aW9uID0gJ2Zha2UgZGVzY3JpcHRpb24nCmtvbF9zeW50aCRMYWJlbCA9ICdLT0wnCmtvbF9zeW50aCA9IGtvbF9zeW50aFssIC0yOV0KCiMgY3JlYXRlIGEgZnVsbCBuZXcgZGF0YQpiYWxhbmNlX2RhdGEgPSBiaW5kX3Jvd3MoZnVsbDMsIHRvbF9zeW50aCwga29sX3N5bnRoKQp0YWJsZShiYWxhbmNlX2RhdGEkTGFiZWwpCmBgYAoKIAojIyMgMTEuIFRyYWluIG1vZGVsIG3hu5tpIHbhu5tpIGThu68gbGnhu4d1IG3hu5tpCgpTYXUga2hpIMSRw6MgdOG6oW8gZOG7ryBsaeG7h3UgZ2nhuqMgxJHhu4Mga2jhuq9jIHBo4bulYyBz4buxIG3huqV0IGPDom4gYuG6sW5nIGdp4buvYSBjw6FjIGNsYXNzIHRhIGxvYWQgZOG7ryBsaeG7h3UgdsOgbyBtw6F5CmBgYHtyfQojIGxvYWQgZGF0YQpkYXQgPSByZWFkLnhsc3goJ2NsYXNzX2JhbGFuY2VkLnhsc3gnLCBzaGVldEluZGV4ID0gMSkgIyByZWFkIGRhdGEgd2l0aCBsYWJlbCBhbmQgbWV0YSBpbmZvcm1hdGlvbgpkYXQgPSBkYXRbLC0xXQpgYGAKCkLhu59pIHbDrCBjw7Mgbmjhu69uZyB0aGFuZyDEkW8ga2jDtG5nIGNodeG6qW4gbsOqbiB0YSBwaOG6o2kgxJFpIHF1eSDEkeG7lWkgbOG6oWksIHbDrCDEkcOieSBsw6AgYmnhur9uIHF1YW4gdHLhu41uZyBuaMawIMSRw6MgbsOqdSByYSDhu58gbeG7pWMgNywgdsOtIGThu6UgbmjGsCB04buVbmcgc+G7kSBsaWtlIHRyw6puIHThuqV0IGPhuqMgcGjhuqNpIGzhuqV5IHRow6BuaCB0cnVuZyBiw6xuaCBz4buRIGxpa2UgdHLDqm4gbeG7mXQgcG9zdCwgdMawxqFuZyB04buxIGNobyBjb21tZW50IHbDoCBpbWFnZSB0csOqbiBQb3N0CgpgYGB7cn0KIyBhZGp1c3QgdGhlIHZhcmlhYmxlIGZvciBtb3JlIG9iamVjdGl2aXR5CmRhdCA9IGRhdCAlPiUgbXV0YXRlKGxpa2VzUGVyUG9zdCA9IHRvdGFsTGlrZXNQb3N0cyAvICh0b3RhbFBvc3RzICsxICksCiAgICAgICAgICAgICAgICAgICAgIGNvbW1lbnRzUGVyUG9zdCA9dG90YWxDb21tZW50c1Bvc3RzIC8gKHRvdGFsUG9zdHMgKyAxKSwKICAgICAgICAgICAgICAgICAgICAgaW1hZ2VzUGVyUG9zdCA9IGltYWdlc19jb3VudF9wb3N0IC8gKHRvdGFsUG9zdHMgKyAxKSkKYGBgCgoKQ2hpYSB0aMOgbmggdOG6rXAgdHJhaW4gc2V0IHbDoCB0ZXN0IHNldCwgdOG7iSBsw6ogNyA6IDMuCgpgYGB7cn0Kc2V0LnNlZWQoMTMpCnRyYWluX2RhdGFfaW5kZXgyID0gc2FtcGxlKDE6IG5yb3coZGF0KSwgcm91bmQobnJvdyhkYXQpKi43KSkgIyBjcmVhdGUgYSByYW5kb20gaW5kZXggaW4gODE4IG9ic2VydmF0aW9ucywgd2l0aCB0aGUgNzozIHJhdGlvCgojIGNyZWF0ZSB0cmFpbm5pbmcgZGF0YSBhbmQgZm9ybWF0CnRyYWluX2RhdGEyID0gZGF0W3RyYWluX2RhdGFfaW5kZXgyLF0KdHJhaW5fZGF0YTIgPSB0cmFpbl9kYXRhMiAlPiUgc2VsZWN0KC11c2VyX2lkLCAtTmFtZSwgLURlc2NyaXB0aW9uLCAtdG90YWxMaWtlc1Bvc3RzLCAtdG90YWxDb21tZW50c1Bvc3RzLCAtaW1hZ2VzX2NvdW50X3Bvc3QpCnRyYWluX2RhdGEyJExhYmVsID0gYXMuY2hhcmFjdGVyKHRyYWluX2RhdGEyJExhYmVsKQp0cmFpbl9kYXRhMiRMYWJlbCA9IGFzLmZhY3Rvcih0cmFpbl9kYXRhMiRMYWJlbCkKCiMgY3JlYXRlIHRlc3RpbmcgZGF0YSBhbmQgZm9ybWF0CnRlc3RfZGF0YTIgPSBkYXRbLXRyYWluX2RhdGFfaW5kZXgyLF0KdGVzdF9kYXRhMiA9IHRlc3RfZGF0YTIgJT4lIHNlbGVjdCgtdXNlcl9pZCwgLU5hbWUsIC1EZXNjcmlwdGlvbiwgLXRvdGFsTGlrZXNQb3N0cywgLXRvdGFsQ29tbWVudHNQb3N0cywgLWltYWdlc19jb3VudF9wb3N0KQp0ZXN0X2RhdGEyJExhYmVsID0gYXMuY2hhcmFjdGVyKHRlc3RfZGF0YTIkTGFiZWwpCnRlc3RfZGF0YTIkTGFiZWwgPSBhcy5mYWN0b3IodGVzdF9kYXRhMiRMYWJlbCkKYGBgCgpDaOG6oXkgbcO0IGjDrG5oIHbhu5tpIGPDoWMgdGhhbSBz4buRIG3hurdjIMSRaW5oIGNo4buJIHRoYXkgxJHhu5VpIHPhu5EgY8OieSBi4bqxbmcgMTAwMCBjw6J5LgoKYGBge3J9CgpzZXQuc2VlZCgxMykKCm1fMTAwMCA9IHJhbmRvbUZvcmVzdChMYWJlbCB+IC4gLCBkYXRhID0gdHJhaW5fZGF0YTIsIGltcG9ydGFuY2UgPSBULCBudHJlZSA9IDEwMDAgKQoKIyBjb21wYXJlIHRoZSBwcmVkaWN0ZWQgbGFiZWwgd2l0aCB0aGUgYWN0dWFsIGxhYmVsCnJlc3VsdF9tXzEwMDAgPSBwcmVkaWN0KG1fMTAwMCwgbmV3ZGF0YSA9IHRlc3RfZGF0YTIpCm1ldHJpYyA9IGNvbmZ1c2lvbk1hdHJpeChyZXN1bHRfbV8xMDAwLCB0ZXN0X2RhdGEyJExhYmVsLCBtb2RlID0gJ2V2ZXJ5dGhpbmcnKQoKCmBgYAoKIyMjIDEyLiDEkMOhbmggZ2nDoSBr4bq/dCBxdeG6oyBj4bunYSBtw7QgaMOsbmggbeG7m2kKCiMjIyMgS+G6v3QgcXXhuqMgY+G7p2EgdOG6rXAgdHJhaW4gc2V0CgpgYGB7cn0KcHJpbnQobV8xMDAwKQpgYGAKClRhIGPDsyB0aOG7gyB0aOG6pXkgxJHGsOG7o2MgxJHhu5kgY2jDrW5oIHjDoWMgdHLDqm4gdOG6rXAgdHJhaW4gc2V0IGzDoCBy4bqldCBjYW8sIGtow7RuZyBjw7Mgc2FpIHNvdCBjaG8gbmjDs20gRkFOLCB2w6AgaOG6p3UgbmjGsCBsw6AgY8WpbmcgdGjhur8gY2hvIG5ow7NtIEtPTC4gQ2jhu4kgY8OzIG5ow7NtIFRPTCBsw6Agc2FpIHPDs3Qga2hv4bqjbmcgMC4wNDYuIFbDoCB04buVbmcgZXJyb3IgcmF0ZSDGsOG7m2MgdMOtbmggbMOgIDEuMjYgcGjhuqduIHRyxINtIHRyw6puIHThuq1wIE9PQi4KCgojIyMjIEvhur90IHF14bqjIHRyw6puIHThuq1wIHRlc3Qgc2V0CgpgYGB7cn0KcHJpbnQobWV0cmljKQpgYGAKCgoqKkNvbmZ1c2lvbiBtYXRyaXgqKgoKYGBge3J9CnRhYmxlKHRlc3RfZGF0YTIkTGFiZWwpCm1ldHJpYyR0YWJsZQpgYGAKClRow6wgdGEgY8OzIHRo4buDIHRo4bqleSBsw6AgMjIyIMSR4buRaSB0xrDhu6NuZyDEkcaw4bujYyBn4bqvbiBuaMOjbiBsw6AgRkFOIMSRw6MgxJHGsOG7o2MgcGjDom4gbG/huqFpIGNow61uaCB4w6FjIHR1eeG7h3QgxJHhu5FpLiAKCmPDsyAxNDEgxJHhu5FpIHTGsOG7o25nIMSRxrDhu6NjIGfhuq9uIG5o4bq1biBLT0wgY2jhu4kgY8OzIDIgxJHhu5FpIHTGsOG7o25nIGLhu4sgcGjDom4gbG/huqFpIGNo4buHY2ggcmEgdGjDoG5oIFRPTC4KClbDoCAxMTIgxJHhu5FpIHTGsOG7o25nIMSRxrDhu6NjIGfhuq9uIG5o4bq1biBUT0wgY2jhu4kgY8OzIDQgxJHhu5FpIHTGsOG7o25nIGLhu4sgcGjDom4gbG/huqFpIHRow6BuaCAxIGNobyBGQU4gdsOgIDMgY2hvIEtPTAoKKipUaOG7kW5nIGvDqiBjaHVuZyB24buBIG3DtCBow6xuaCoqCgohW10oaW1hZ2VzL292ZXJhbGxfc3RhdGlzdGljLnBuZykKCgoKYGBge3J9CnByaW50KHByb3AudGFibGUodGFibGUodGVzdF9kYXRhMiRMYWJlbCkpKQpgYGAKCgpNw7QgaMOsbmggbsOgeSBjw7MgxJHhu5kgY2jDrW5oIHjDoWMgQWNjdXJhY3kgcmF0ZSBsw6AgMC45ODcgcuG6pXQgbMOgIGNhbwoKS2hpIHNvIHPDoW5oIHbhu5tpIGzhuqFpIHbhu5tpIGNo4buJIHPhu5EgTm8gSW5mb3JtYXRpb24gUmF0ZSwgbmjGsCDEkcOjIG7Ds2kg4bufIG3hu6VjIDguIENo4buJIHPhu5EgbsOgeSBjaG8gdGjhuqV5LCBu4bq/dSBraMO0bmcgeMOpdCDEkeG6v24gYuG6pXQga+G7gyB0aMO0bmcgdGluIG7DoG8gbmdvw6BpIHBow6JuIGLhu5EgdOG7iSBs4buHIGPhu6dhIDMgbmjDs20gRkFOLCBLT0wgdsOgIFRPTCB0aMOsIG7hur91IGNo4buNbiBuZ+G6q3Ugbmhpw6puLCBt4buXaSAxMDAgbOG6p24gdGjDrCBjw7MgNDYgbOG6p24gbMOgIGNo4buNbiDEkcaw4bujYyB0csO6bmcgRkFOLCAzMCBs4bqnbiBsw6AgY+G7jW4gxJHGsOG7o2MgdHLDum5nIEtPTCB2w6AgMjMgbOG6p24gbMOgIFRPTCAuCgrEkOG7mSBjaMOtbmggeMOhYyBj4bunYSBtw7QgaMOsbmggQWNjdXJhY3kgcmF0ZSBsw6AgMC45ODcgY2hvIHRo4bqleSBtw7QgaMOsbmggxJFhbmcgxJFpIMSRw7puZyBoxrDhu5tuZyB2w6AgbMOgbSB2aeG7h2MgcuG6pXQgdOG7kXQuCgoKKipQcmVjaXNvbiB2w6AgUmVjYWxsKioKCiFbXShpbWFnZXMvc3RhdGlzdGljX2NsYXNzLnBuZykKCuG7niDEkcOieSB0YSBjw7MgdGjhu4MgdGjhuqV5IGzDoCDEkeG7mSBjaMOtbmggeMOhYyBj4bunYSBtw7QgaMOsbmggcGjDom4gbG/huqFpIGNobyAzIGNsYXNzIGzDoCBGQU4sIEtPTCB2w6AgVE9MIGzDoCBy4bqldCBjYW8uCgpDbGFzcyBGQU4gY8OzIMSR4buZIGNow61uaCB4w6FjIFByZWNpc2lvbiA6IDAuOTk1NSB2w6AgbeG7qWMgxJHhu5kga2jDtG5nIGLhu48gc8OzdCBSZWNhbGw6IGzDoCAxLCBjw7MgbmdoxKlhIGzDoCBtw6F5IG7DsyDEkcOjIGzhu41jIHJhIDEwMCBwaOG6p24gdHLEg20gY8OhYyDEkeG7kWkgdMaw4bujbmcgbMOgIEZBTiAoUmVjYWxsKSB2w6AgUGjDom4gbG/huqFpIGNow61uaCB4w6FjIMSR4bq/biA5OSw1IHBo4bqnbiB0csSDbSBsw6AgKHByZWNpc2lvbikKCkNsYXNzIEtPTCBjw7MgxJHhu5kgY2jDrW5oIHjDoWMgUHJlY2lzaW9uIDogMC45Nzg5IHbDoCBt4bupYyDEkeG7mSBraMO0bmcgYuG7jyBzw7N0IFJlY2FsbDogbMOgIDAuOTg1CgpDbGFzcyBUT0wgduG7m2kgxJHhu5kgY2jDrW5oIHjDoWMgbMOgIDAuOTgxOCB2w6AgbeG7qWMgxJHhu5kga2jDtG5nIGLhu48gc8OzdCBsw6A6IDAuOTY0MwoKVGjDrCBjw6FpIMSRaeG7gXUgbsOgeSBjaG8gdGjhuqV5IG3DtCBow6xuaCBob+G6oXQgxJHhu5luZyBy4bqldCBjaMOtbmggeMOhYwoKKipDw6FjIMSR4buRaSB0xrDhu6NuZyBi4buLIHBow6JuIGxv4bqhaSBzYWkqKgoKCmBgYHtyfQplcnJvcl8xMDAwID0gZGF0Wy10cmFpbl9kYXRhX2luZGV4MixdW3doaWNoKHJlc3VsdF9tXzEwMDAgIT0gdGVzdF9kYXRhMiRMYWJlbCksIF0KZXJyb3JfMTAwMCRwcmVkaWN0X2xhYmVsID0gcmVzdWx0X21fMTAwMFt3aGljaChyZXN1bHRfbV8xMDAwICE9IHRlc3RfZGF0YTIkTGFiZWwpXQpjb21wYXJlXzEwMDAgPSBlcnJvcl8xMDAwICU+JSBzZWxlY3QodXNlcl9pZCwgTmFtZSwgRGVzY3JpcHRpb24sIExhYmVsLCBwcmVkaWN0X2xhYmVsKQoKcHJpbnQoY29tcGFyZV8xMDAwKQpwcmludChlcnJvcl8xMDAwKQpgYGAKCioqQVVDIChBcmVhIHVuZGVyIFJPQyBjdXJ2ZSkqKgpgYGB7cn0KIyBjYWxjdWxhdGUgdGhlIGF1YwoKYWN0dWFsX21fMTAwMCA9IGFzLm51bWVyaWModGVzdF9kYXRhMiRMYWJlbCApCnByZWRpY3RlZF9tXzEwMDAgPSBhcy5udW1lcmljKHJlc3VsdF9tXzEwMDApCnJlc3VsdF9yb2NfbV8xMDAwID0gbXVsdGljbGFzcy5yb2MoYWN0dWFsX21fMTAwMCwgcHJlZGljdGVkX21fMTAwMCkKcmVzdWx0X3JvY19tXzEwMDAKYXVjKHJlc3VsdF9yb2NfbV8xMDAwKQpgYGAKCuG7niDEkcOieSB0YSB0aOG6pXkgxJHhu5kgY2jDrW5oIHjDoWMgdOG7lW5nIGjhu6NwIGNobyAzIGNsYXNzIGzDoCAwLjk4ODcgbMOgIHLhuqV0IHThu5F0CgpDw7MgdGjhu4MgY29pIGjDrG5oIHNhdToKCmBgYHtyfQojIHBsb3QgdGhlIGF1YwpycyA8LSByZXN1bHRfcm9jX21fMTAwMFtbJ3JvY3MnXV0KcGxvdC5yb2MocnNbWzFdXSwgbWFpbiA9ICJBVUMgb2YgRkFOIikKcGxvdC5yb2MocnNbWzJdXSwgbWFpbiA9ICJBVUMgb2YgS09MIikKcGxvdC5yb2MocnNbWzNdXSwgbWFpbiA9ICJBVUMgb2YgVE9MIikKCgpwbG90LnJvYyhyc1tbM11dLCBtYWluID0gIkFVQyBvZiAzIGNsYXNzZXMiKQpzYXBwbHkoMTpsZW5ndGgocnMpLGZ1bmN0aW9uKGkpIGxpbmVzLnJvYyhyc1tbaV1dLGNvbD1pLCBsd2QgPSAuOCkpCmBgYAoKCgoKIyMjIDE0LiBU4buRaSDGsHUgaMOzYSBtw7QgaMOsbmggCgoqKkPDoWMgYmnhur9uIHF1YW4gdHLhu41uZyoqCmBgYHtyfQp2YXJJbXBQbG90KG1fMTAwMCwgY2V4ID0uOCkKYGBgCgoqKkxlYXJuaW5nIGN1cnZlKioKYGBge3J9CgogIHBsb3QobV8xMDAwLCBtYWluID0gJ2Vycm9yIHJhdGUgd2l0aCAxMDAwIHRyZWVzJykKICBsZWdlbmQoInRvcHJpZ2h0IiwgY29sbmFtZXMobV8xMDAwJGVyci5yYXRlKSxjb2w9MTo0LGNleD0wLjgsZmlsbD0xOjQpCmBgYAoKYGBge3J9CgojIGNvbnZlcnQgZm9ybSBtYXRyaXggdG8gZGF0YSBmcmFtZQplcnIuZGYgPSBtXzEwMDAkZXJyLnJhdGUgJT4lIGFzLmRhdGEuZnJhbWUoKQoKI2ZpbmQgdGhlIG51bWJlciBvZiB0cmVlIHRoYXQgY29ycmVzcG9uZGVudCB0byBtaW4gZXJyb3IgcmF0ZQp3aGljaC5taW4oZXJyLmRmJE9PQikKCmBgYAoK4bueIMSRw6J5IGPDsyB0aOG7gyB0aOG6pXkgc2FpIHPhu5EgdGjhuqVwIG5o4bqldCDhu58gMTIxIGPDonkKCkNo4bqheSBtw7QgaMOsbmggbeG7m2kgduG7m2kgMTIxIGPDonkgduG7m2kgY8OhYyB0aGFtIHPhu5EgbeG6t2MgxJHhu4tuaApgYGB7cn0KCnNldC5zZWVkKDEzKQoKbV8xMjEgPSByYW5kb21Gb3Jlc3QoTGFiZWwgfiAuICwgZGF0YSA9IHRyYWluX2RhdGEyLCBpbXBvcnRhbmNlID0gVCwgbnRyZWUgPSAxMjEgKQoKIyBjb21wYXJlIHRoZSBwcmVkaWN0ZWQgbGFiZWwgd2l0aCB0aGUgYWN0dWFsIGxhYmVsCnJlc3VsdF9tXzEyMSA9IHByZWRpY3QobV8xMjEsIG5ld2RhdGEgPSB0ZXN0X2RhdGEyKQptZXRyaWNfMTIxID0gY29uZnVzaW9uTWF0cml4KHJlc3VsdF9tXzEyMSwgdGVzdF9kYXRhMiRMYWJlbCwgbW9kZSA9ICdldmVyeXRoaW5nJykKcHJpbnQobWV0cmljXzEyMSkKCmBgYAoKVGjhu7FjIGhp4buHbiBncmlkIHNlYXJjaCDEkeG7gyB0w6xtIGPDoWMgdGhhbSBz4buRIHThu5FpIMawdQoKYGBge3IsIGV2YWwgPSBGLCBlY2hvID0gVH0KCiMgZnVsbCBncmlkIHNlYXJjaAoKaHlwZXJfZ3JpZCA8LSBleHBhbmQuZ3JpZCgKICBtdHJ5ICAgICAgID0gc2VxKDIsIDI4LCBieSA9IDEpLAogIG5vZGVfc2l6ZSAgPSBzZXEoMiwgMjgsIGJ5ID0gMSksCiAgc2FtcGxlX3NpemUgPSBjKC41NSwgLjYzMiwgLjcwLCAuODApLAogIE9PQl9STVNFICAgPSAwCikKCiMgc2VhcmNoaW5nCgpmb3IoaSBpbiAxOm5yb3coaHlwZXJfZ3JpZCkpIHsKICAKICAjIHRyYWluIG1vZGVsCiAgbW9kZWwgPC0gcmFuZ2VyKAogICAgZm9ybXVsYSAgICAgICAgID0gTGFiZWwgfiAuLCAKICAgIGRhdGEgICAgICAgICAgICA9IHRyYWluX2RhdGEyLCAKICAgIG51bS50cmVlcyAgICAgICA9IDEyMSwKICAgIG10cnkgICAgICAgICAgICA9IGh5cGVyX2dyaWQkbXRyeVtpXSwKICAgIG1pbi5ub2RlLnNpemUgICA9IGh5cGVyX2dyaWQkbm9kZV9zaXplW2ldLAogICAgc2FtcGxlLmZyYWN0aW9uID0gaHlwZXJfZ3JpZCRzYW1wbGVfc2l6ZVtpXSwKICAgIHNlZWQgICAgICAgICAgICA9IDEyCiAgKQogIAogICMgYWRkIE9PQiBlcnJvciB0byBncmlkCiAgaHlwZXJfZ3JpZCRPT0JfUk1TRVtpXSA8LSBzcXJ0KG1vZGVsJHByZWRpY3Rpb24uZXJyb3IpCn0KCmBgYAoKWGVtIGPDoWMgdGhhbSBz4buRIHThu5FpIMawdSBuaOG6pXQKYGBge3J9CiMgbG9vayBhdCBsb3dlc3QgUk1TRSAKaHlwZXJfZ3JpZCAlPiUgCiAgZHBseXI6OmFycmFuZ2UoT09CX1JNU0UpICU+JQogIGhlYWQoMTApCmBgYAoKS+G6v3QgcXXhuqMgY2hvIHRo4bqleSBzYXUga2hpIHRo4buxYyBoaeG7h24gdMOsbSBraeG6v20gdHLDqm4gMjkxNiBs4bqnbiBjaOG6oXkgbcO0IGjDrG5oIHbhu5tpIGPDoWMgdGhhbSBz4buRIHRhIMSRxrDhu6NjIGLhu5kgdGhhbSBz4buRIGPDsyBPT0IgdGjhuqVwIG5o4bqldCBsw6AKCm10cnkgPTMKCm5vZGVfc2l6ZSA9IDIKCgoKVGEgY2jhuqF5IG3DtCBow6xuaCBt4bubaSB24bubaSBjw6FjIHRoYW0gc+G7kSB04buRaSDGsHUgduG7q2EgdMOsbSDEkcaw4bujYwoKYGBge3J9CnNldC5zZWVkKDEzKQpvcHRpbWl6ZWRfbW9kZWwgPSByYW5kb21Gb3Jlc3QoZm9ybXVsYSA9IExhYmVsIH4uLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGRhdGEgPSB0cmFpbl9kYXRhMiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG50cmVlID0gMTIxLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbXRyeSA9IDMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBub2Rlc2l6ZSA9IDMKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKQoKbV9vcCA9IHByZWRpY3Qob3B0aW1pemVkX21vZGVsLCBuZXdkYXRhID0gdGVzdF9kYXRhMikKY29uZnVzaW9uTWF0cml4KG1fb3AsIHRlc3RfZGF0YTIkTGFiZWwsIG1vZGUgPSAnZXZlcnl0aGluZycpCgpgYGAKCk1vZGVsIG3hu5tpIGvhur90IHF14bqjIGPFqW5nIGtow7RuZyBoxqFuIGfDrCBtb2RlbCBjxakgduG7m2kgdGhhbSBz4buRIG3hurdjIMSR4buLbmggdsOgIGNo4bqheSB24bubaSAxMDAwIGPDonkK