1 Triển khai mô hình ML

Gồm 2 bước:

  • Huấn luyện mô hình: Lựa chọn mô hình tốt nhất. Bước này thực hiện 1 lần (chỉ thực hiện lại khi mô hình giảm khả năng dự báo)

  • Vận hành mô hình: Dựa trên kết quả sau khi huấn luyện, cung cấp API cho các đơn vị khác sử dụng.

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

2.1 Dữ liệu

  • Sử dụng bộ dữ liệu demo: german_credit
  • Biến phụ thuộc: credit_risk: khách hàng good hoặc bad
  • 3 biến độc lập: amount: số tiền giải ngân, purpose: mục đích vay, age: tuổi của khách hàng.
data = tsk("german_credit")$data()
data = data[, c("credit_risk", "amount", "purpose", "age")]
task = TaskClassif$new("boston", backend = data, target = "credit_risk")
head(data, 5)
##    credit_risk amount             purpose age
## 1:        good   1169 furniture/equipment  67
## 2:         bad   5951 furniture/equipment  22
## 3:        good   2096             repairs  49
## 4:        good   7882          car (used)  45
## 5:         bad   4870              others  53

2.2 Quy trình thực hiện huấn luyện mô hình

2.2.1 Xử lý missing

  • Với biến định lượng, nội suy bằng giá trị trung vị
  • Với biến định tính thêm 1 level mới tên là .MISSING

2.2.2 Chuyển đổi woe

2.2.3 Thuật toán sử dụng để huấn luyện mô hình lightgbm

g = po("imputemedian") %>>%
  po("imputeoor") %>>%
  po("fixfactors") %>>%
  po("encodeimpact") %>>% # woe transformation
  lrn("classif.lightgbm")

gl = GraphLearner$new(g) # convert to graph

g$plot(html = TRUE) %>% visNetwork::visInteraction()

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

Kết hợp các quy trình và số liệu để ra được mô hình tốt nhất. Để không làm phức tạp, tại bước này sẽ sử dụng các tham số mặc định.

gl$train(task)
## [LightGBM] [Info] Number of positive: 700, number of negative: 300
## [LightGBM] [Warning] Auto-choosing row-wise multi-threading, the overhead of testing was 0.000053 seconds.
## You can set `force_row_wise=true` to remove the overhead.
## And if memory is not enough, you can set `force_col_wise=true`.
## [LightGBM] [Info] Total Bins 328
## [LightGBM] [Info] Number of data points in the train set: 1000, number of used features: 4
## [LightGBM] [Info] [binary:BoostFromScore]: pavg=0.700000 -> initscore=0.847298
## [LightGBM] [Info] Start training from score 0.847298

2.4 Lưu mô hình

Quá trình huấn luyện mô hình chỉ thực hiện 1 lần nên kết quả từ mô hình (object) sẽ được lưu lại dưới dạng 1 file .rds để phục vụ cho việc dự báo về sau.

# function save object
fnc_save_lightgbm <- function(graph_learner = NULL, model = NULL, file = NULL){
  # save lightgbm model
  saveRDS.lgb.Booster(model, paste0(file,"_model.rds"))
  # save graph learner
  saveRDS(graph_learner, paste0(file,"_gl.rds"))
}

# Save model
fnc_save_lightgbm(gl, gl$model$classif.lightgbm$model, 'lightgbm_test')

# save features info
feature_info = list(
  feature_names = task$feature_names,
  feature_types = task$feature_types,
  levels = task$levels()
)

saveRDS(feature_info, "feature_info.rds")

2.5 Thực hiện dự báo

Trước khi tạo API, ta kiểm tra kết quả dự báo của mô hình bằng 1 ví dụ. Đầu ra của mô hình có thể là hạng hoặc điểm số, xác suất vỡ nợ hoặc cả 3 tùy thuộc vào mục đích sử dụng. Phần ví dụ này đầu ra sẽ là điểm số.

newdata = data.table(amount = 1169, purpose = 'repairs', age = 20)
pred = as.data.table(gl$predict_newdata(newdata))
score = 1000 - pred$prob.bad * 1000

Điểm số: 659.8626762

3 Vận hành mô hình

Trong phần này chúng ta sẽ làm 1 số việc như:

  • sử dụng đầu vào là kết quả mô hình đã được huấn luyện ở trên bằng cách đọc file .rds đã được lưu lại ở trên.

  • Định dạng dữ liệu đầu vào để phù hợp với input của mô hình

  • Và tạo API để giao tiếp với các máy tính khác.

3.1 Định dạng dữ liệu đầu vào

Dữ liệu đầu vào bao gồm các biến dạng numeric, text, … cần phải được định dạng trước để máy có thể hiểu. Hàm fix_feature_types() dưới đây sẽ được sử dụng để định dạng dữ liệu đầu vào.

fix_feature_types <- function(feature, feature_name, feature_info) {
  id = match(feature_name, feature_info$feature_names)
  feature_type = feature_info$feature_types$type[id]
  switch(
    feature_type,
    "logical"   = as.logical(feature),
    "integer"   = as.integer(feature),
    "numeric"   = as.numeric(feature),
    "character" = as.character(feature),
    "factor"    = factor(feature, levels = feature_info$levels[[feature_name]],
                         ordered = FALSE),
    "ordered"   = factor(feature, levels = feature_info$levels[[feature_name]],
                         ordered = TRUE),
    "POSIXct"   = as.POSIXct(feature)
  )
}

3.2 Gọi mô hình đã được huấn luyện ở trên

Để có dự báo chúng ta sử dụng POST request gửi dữ liệu dưới dạng JSON đến máy chủ. Sau khi nhận dữ liệu body từ POST request, máy chủ sẽ thực hiện tính toán, kết quả trả ra là điểm số định dạng JSON.

#* @post /predict_credit_risk
function(req) {
  # get the JSON string from the post body
  newdata = fromJSON(req$postBody, simplifyVector = FALSE)
  # expect either JSON objects in an array or nested JSON objects
  newdata = rbindlist(newdata, use.names = TRUE)
  # convert all features in place to their expected feature_type
  newdata[, colnames(newdata) := mlr3misc::pmap(list(.SD, colnames(newdata)),
                                                fix_feature_types,
                                                feature_info = feature_info)]
  # predict and return as a data.table
  pred = as.data.table(gl$predict_newdata(newdata))
  score = 1000 - pred$prob.bad * 1000
  return(score)
  # or only the numeric values
  # gl$predict_newdata(newdata)$response
}

3.3 Tạo API

Sử dụng package plumber cho để cài đặt web service. Trong phần này sẽ chạy phần code để dự báo (predict_gl.R) và khai báo host, port.

library(plumber)
r = plumb(file = "R/predict_gl.R", dir ='R/model_deploy')
r$run(port = 1030, host = "0.0.0.0")

3.4 Gửi request body

Ví dụ 1 trường hợp gửi dữ liệu đến máy chủ và kết quả nhận được.

newdata = '[{"amount":1169, "purpose":"repairs", "age":"20"}]'
resp = httr::POST(url = "http://127.0.0.1:1030/predict_credit_risk",
                  body = newdata, encode = "json")
httr::content(resp)
LS0tDQp0aXRsZTogIkRlcGxveSBtb2RlbCBMaWdodEdCTSBpbiBSIHdpdGggcGx1bWJlciINCmF1dGhvcjogIk5ndXnhu4VuIE5n4buNYyBCw6xuaCINCmRhdGU6ICJgciBTeXMuRGF0ZSgpYCINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgIGNvZGVfZm9sZGluZzogc2hvdw0KICAgIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJkZWZhdWx0Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KICAgIGRldjogJ3N2ZycNCmVkaXRvcl9vcHRpb25zOiANCiAgY2h1bmtfb3V0cHV0X3R5cGU6IGNvbnNvbGUNCi0tLQ0KDQpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgd2FybmluZyA9IEYpDQpsaWJyYXJ5KGRhdGEudGFibGUpDQpsaWJyYXJ5KGxpZ2h0Z2JtKQ0KbGlicmFyeShkcGx5cikNCmxpYnJhcnkoanNvbmxpdGUpDQpsaWJyYXJ5KG1scjMpDQpsaWJyYXJ5KG1scjNwaXBlbGluZXMpDQpsaWJyYXJ5KG1scjNleHRyYWxlYXJuZXJzKQ0KYGBgDQoNCiMgVHJp4buDbiBraGFpIG3DtCBow6xuaCBNTA0KDQpH4buTbSAyIGLGsOG7m2M6DQoNCi0gSHXhuqVuIGx1eeG7h24gbcO0IGjDrG5oOiBM4buxYSBjaOG7jW4gbcO0IGjDrG5oIHThu5F0IG5o4bqldC4gQsaw4bubYyBuw6B5IHRo4buxYyBoaeG7h24gMSBs4bqnbiAoY2jhu4kgdGjhu7FjIGhp4buHbiBs4bqhaSBraGkgbcO0IGjDrG5oIGdp4bqjbSBraOG6oyBuxINuZyBk4buxIGLDoW8pIA0KDQotIFbhuq1uIGjDoG5oIG3DtCBow6xuaDogROG7sWEgdHLDqm4ga+G6v3QgcXXhuqMgc2F1IGtoaSBodeG6pW4gbHV54buHbiwgY3VuZyBj4bqlcCBBUEkgY2hvIGPDoWMgxJHGoW4gduG7iyBraMOhYyBz4butIGThu6VuZy4NCg0KIVtdKGRlcGxveV9pbWFnZS5QTkcpDQoNCiMgSHXhuqVuIGx1eeG7h24gbcO0IGjDrG5oDQoNCiMjIEThu68gbGnhu4d1DQoNCi0gU+G7rSBk4bulbmcgYuG7mSBk4buvIGxp4buHdSBkZW1vOiBgZ2VybWFuX2NyZWRpdGANCi0gQmnhur9uIHBo4bulIHRodeG7mWM6IGBjcmVkaXRfcmlza2A6IGtow6FjaCBow6BuZyBfZ29vZF8gaG/hurdjIF9iYWRfDQotIDMgYmnhur9uIMSR4buZYyBs4bqtcDogYGFtb3VudGA6IHPhu5EgdGnhu4FuIGdp4bqjaSBuZ8OibiwgYHB1cnBvc2VgOiBt4bulYyDEkcOtY2ggdmF5LCBgYWdlYDogdHXhu5VpIGPhu6dhIGtow6FjaCBow6BuZy4NCg0KYGBge3J9DQpkYXRhID0gdHNrKCJnZXJtYW5fY3JlZGl0IikkZGF0YSgpDQpkYXRhID0gZGF0YVssIGMoImNyZWRpdF9yaXNrIiwgImFtb3VudCIsICJwdXJwb3NlIiwgImFnZSIpXQ0KdGFzayA9IFRhc2tDbGFzc2lmJG5ldygiYm9zdG9uIiwgYmFja2VuZCA9IGRhdGEsIHRhcmdldCA9ICJjcmVkaXRfcmlzayIpDQpoZWFkKGRhdGEsIDUpDQpgYGANCg0KIyMgUXV5IHRyw6xuaCB0aOG7sWMgaGnhu4duIGh14bqlbiBsdXnhu4duIG3DtCBow6xuaA0KDQojIyMgWOG7rSBsw70gbWlzc2luZyANCg0KLSBW4bubaSBiaeG6v24gxJHhu4tuaCBsxrDhu6NuZywgbuG7mWkgc3V5IGLhurFuZyBnacOhIHRy4buLIHRydW5nIHbhu4sNCi0gVuG7m2kgYmnhur9uIMSR4buLbmggdMOtbmggdGjDqm0gMSBsZXZlbCBt4bubaSB0w6puIGzDoCAuTUlTU0lORw0KDQojIyMgQ2h1eeG7g24gxJHhu5VpIHdvZSAgICAgDQojIyMgVGh14bqtdCB0b8OhbiBz4butIGThu6VuZyDEkeG7gyBodeG6pW4gbHV54buHbiBtw7QgaMOsbmggYGxpZ2h0Z2JtYA0KDQpgYGB7cn0NCmcgPSBwbygiaW1wdXRlbWVkaWFuIikgJT4+JQ0KICBwbygiaW1wdXRlb29yIikgJT4+JQ0KICBwbygiZml4ZmFjdG9ycyIpICU+PiUNCiAgcG8oImVuY29kZWltcGFjdCIpICU+PiUgIyB3b2UgdHJhbnNmb3JtYXRpb24NCiAgbHJuKCJjbGFzc2lmLmxpZ2h0Z2JtIikNCg0KZ2wgPSBHcmFwaExlYXJuZXIkbmV3KGcpICMgY29udmVydCB0byBncmFwaA0KDQpnJHBsb3QoaHRtbCA9IFRSVUUpICU+JSB2aXNOZXR3b3JrOjp2aXNJbnRlcmFjdGlvbigpDQoNCmBgYA0KDQojIyBIdeG6pW4gbHV54buHbiBtw7QgaMOsbmgNCg0KS+G6v3QgaOG7o3AgY8OhYyBxdXkgdHLDrG5oIHbDoCBz4buRIGxp4buHdSDEkeG7gyByYSDEkcaw4bujYyBtw7QgaMOsbmggdOG7kXQgbmjhuqV0Lg0KxJDhu4Mga2jDtG5nIGzDoG0gcGjhu6ljIHThuqFwLCB04bqhaSBixrDhu5tjIG7DoHkgc+G6vSBz4butIGThu6VuZyBjw6FjIHRoYW0gc+G7kSBt4bq3YyDEkeG7i25oLg0KDQpgYGB7cix3YXJuaW5nPUZBTFNFfQ0KZ2wkdHJhaW4odGFzaykNCmBgYA0KDQojIyBMxrB1IG3DtCBow6xuaA0KDQpRdcOhIHRyw6xuaCBodeG6pW4gbHV54buHbiBtw7QgaMOsbmggY2jhu4kgdGjhu7FjIGhp4buHbiAxIGzhuqduIG7Dqm4ga+G6v3QgcXXhuqMgdOG7qyBtw7QgaMOsbmggKGBvYmplY3RgKSBz4bq9IMSRxrDhu6NjIGzGsHUgbOG6oWkgZMaw4bubaSBk4bqhbmcgMSBmaWxlIF8ucmRzXyDEkeG7gyBwaOG7pWMgduG7pSBjaG8gdmnhu4djIGThu7EgYsOhbyB24buBIHNhdS4NCg0KYGBge3J9DQojIGZ1bmN0aW9uIHNhdmUgb2JqZWN0DQpmbmNfc2F2ZV9saWdodGdibSA8LSBmdW5jdGlvbihncmFwaF9sZWFybmVyID0gTlVMTCwgbW9kZWwgPSBOVUxMLCBmaWxlID0gTlVMTCl7DQogICMgc2F2ZSBsaWdodGdibSBtb2RlbA0KICBzYXZlUkRTLmxnYi5Cb29zdGVyKG1vZGVsLCBwYXN0ZTAoZmlsZSwiX21vZGVsLnJkcyIpKQ0KICAjIHNhdmUgZ3JhcGggbGVhcm5lcg0KICBzYXZlUkRTKGdyYXBoX2xlYXJuZXIsIHBhc3RlMChmaWxlLCJfZ2wucmRzIikpDQp9DQoNCiMgU2F2ZSBtb2RlbA0KZm5jX3NhdmVfbGlnaHRnYm0oZ2wsIGdsJG1vZGVsJGNsYXNzaWYubGlnaHRnYm0kbW9kZWwsICdsaWdodGdibV90ZXN0JykNCg0KIyBzYXZlIGZlYXR1cmVzIGluZm8NCmZlYXR1cmVfaW5mbyA9IGxpc3QoDQogIGZlYXR1cmVfbmFtZXMgPSB0YXNrJGZlYXR1cmVfbmFtZXMsDQogIGZlYXR1cmVfdHlwZXMgPSB0YXNrJGZlYXR1cmVfdHlwZXMsDQogIGxldmVscyA9IHRhc2skbGV2ZWxzKCkNCikNCg0Kc2F2ZVJEUyhmZWF0dXJlX2luZm8sICJmZWF0dXJlX2luZm8ucmRzIikNCmBgYA0KDQojIyBUaOG7sWMgaGnhu4duIGThu7EgYsOhbw0KDQpUcsaw4bubYyBraGkgdOG6oW8gQVBJLCB0YSBraeG7g20gdHJhIGvhur90IHF14bqjIGThu7EgYsOhbyBj4bunYSBtw7QgaMOsbmggYuG6sW5nIDEgdsOtIGThu6UuDQrEkOG6p3UgcmEgY+G7p2EgbcO0IGjDrG5oIGPDsyB0aOG7gyBsw6AgaOG6oW5nIGhv4bq3YyDEkWnhu4NtIHPhu5EsIHjDoWMgc3XhuqV0IHbhu6EgbuG7oyBob+G6t2MgY+G6oyAzIHTDuXkgdGh14buZYyB2w6BvIG3hu6VjIMSRw61jaCBz4butIGThu6VuZy4NClBo4bqnbiB2w60gZOG7pSBuw6B5IMSR4bqndSByYSBz4bq9IGzDoCDEkWnhu4NtIHPhu5EuDQoNCmBgYHtyfQ0KbmV3ZGF0YSA9IGRhdGEudGFibGUoYW1vdW50ID0gMTE2OSwgcHVycG9zZSA9ICdyZXBhaXJzJywgYWdlID0gMjApDQpwcmVkID0gYXMuZGF0YS50YWJsZShnbCRwcmVkaWN0X25ld2RhdGEobmV3ZGF0YSkpDQpzY29yZSA9IDEwMDAgLSBwcmVkJHByb2IuYmFkICogMTAwMA0KDQpgYGANCg0KxJBp4buDbSBz4buROiBgciBzY29yZWANCg0KIyBW4bqtbiBow6BuaCBtw7QgaMOsbmgNCg0KVHJvbmcgcGjhuqduIG7DoHkgY2jDum5nIHRhIHPhur0gbMOgbSAxIHPhu5Egdmnhu4djIG5oxrA6DQoNCi0gc+G7rSBk4bulbmcgxJHhuqd1IHbDoG8gbMOgIGvhur90IHF14bqjIG3DtCBow6xuaCDEkcOjIMSRxrDhu6NjIGh14bqlbiBsdXnhu4duIOG7nyB0csOqbiBi4bqxbmcgY8OhY2ggxJHhu41jIGZpbGUgXy5yZHNfIMSRw6MgxJHGsOG7o2MgbMawdSBs4bqhaSDhu58gdHLDqm4uDQoNCi0gxJDhu4tuaCBk4bqhbmcgZOG7ryBsaeG7h3UgxJHhuqd1IHbDoG8gxJHhu4MgcGjDuSBo4bujcCB24bubaSBpbnB1dCBj4bunYSBtw7QgaMOsbmgNCg0KLSBWw6AgdOG6oW8gQVBJIMSR4buDIGdpYW8gdGnhur9wIHbhu5tpIGPDoWMgbcOheSB0w61uaCBraMOhYy4NCg0KDQojIyDEkOG7i25oIGThuqFuZyBk4buvIGxp4buHdSDEkeG6p3UgdsOgbw0KDQpE4buvIGxp4buHdSDEkeG6p3UgdsOgbyBiYW8gZ+G7k20gY8OhYyBiaeG6v24gZOG6oW5nIG51bWVyaWMsIHRleHQsIC4uLiBj4bqnbiBwaOG6o2kgxJHGsOG7o2MgxJHhu4tuaCBk4bqhbmcgdHLGsOG7m2MgxJHhu4MgbcOheSBjw7MgdGjhu4MgaGnhu4N1LiANCkjDoG0gYGZpeF9mZWF0dXJlX3R5cGVzKClgIGTGsOG7m2kgxJHDonkgc+G6vSDEkcaw4bujYyBz4butIGThu6VuZyDEkeG7gyDEkeG7i25oIGThuqFuZyBk4buvIGxp4buHdSDEkeG6p3UgdsOgby4NCg0KYGBge3J9DQpmaXhfZmVhdHVyZV90eXBlcyA8LSBmdW5jdGlvbihmZWF0dXJlLCBmZWF0dXJlX25hbWUsIGZlYXR1cmVfaW5mbykgew0KICBpZCA9IG1hdGNoKGZlYXR1cmVfbmFtZSwgZmVhdHVyZV9pbmZvJGZlYXR1cmVfbmFtZXMpDQogIGZlYXR1cmVfdHlwZSA9IGZlYXR1cmVfaW5mbyRmZWF0dXJlX3R5cGVzJHR5cGVbaWRdDQogIHN3aXRjaCgNCiAgICBmZWF0dXJlX3R5cGUsDQogICAgImxvZ2ljYWwiICAgPSBhcy5sb2dpY2FsKGZlYXR1cmUpLA0KICAgICJpbnRlZ2VyIiAgID0gYXMuaW50ZWdlcihmZWF0dXJlKSwNCiAgICAibnVtZXJpYyIgICA9IGFzLm51bWVyaWMoZmVhdHVyZSksDQogICAgImNoYXJhY3RlciIgPSBhcy5jaGFyYWN0ZXIoZmVhdHVyZSksDQogICAgImZhY3RvciIgICAgPSBmYWN0b3IoZmVhdHVyZSwgbGV2ZWxzID0gZmVhdHVyZV9pbmZvJGxldmVsc1tbZmVhdHVyZV9uYW1lXV0sDQogICAgICAgICAgICAgICAgICAgICAgICAgb3JkZXJlZCA9IEZBTFNFKSwNCiAgICAib3JkZXJlZCIgICA9IGZhY3RvcihmZWF0dXJlLCBsZXZlbHMgPSBmZWF0dXJlX2luZm8kbGV2ZWxzW1tmZWF0dXJlX25hbWVdXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBvcmRlcmVkID0gVFJVRSksDQogICAgIlBPU0lYY3QiICAgPSBhcy5QT1NJWGN0KGZlYXR1cmUpDQogICkNCn0NCmBgYA0KDQojIyBH4buNaSBtw7QgaMOsbmggxJHDoyDEkcaw4bujYyBodeG6pW4gbHV54buHbiDhu58gdHLDqm4NCg0KxJDhu4MgY8OzIGThu7EgYsOhbyBjaMO6bmcgdGEgc+G7rSBk4bulbmcgUE9TVCByZXF1ZXN0IGfhu61pIGThu68gbGnhu4d1IGTGsOG7m2kgZOG6oW5nIEpTT04gxJHhur9uIG3DoXkgY2jhu6cuDQpTYXUga2hpIG5o4bqtbiBk4buvIGxp4buHdSBib2R5IHThu6sgUE9TVCByZXF1ZXN0LCBtw6F5IGNo4bunIHPhur0gdGjhu7FjIGhp4buHbiB0w61uaCB0b8Ohbiwga+G6v3QgcXXhuqMgdHLhuqMgcmEgbMOgIMSRaeG7g20gc+G7kSDEkeG7i25oIGThuqFuZyBKU09OLg0KDQpgYGB7ciwgZXZhbD1GQUxTRX0NCiMqIEBwb3N0IC9wcmVkaWN0X2NyZWRpdF9yaXNrDQpmdW5jdGlvbihyZXEpIHsNCiAgIyBnZXQgdGhlIEpTT04gc3RyaW5nIGZyb20gdGhlIHBvc3QgYm9keQ0KICBuZXdkYXRhID0gZnJvbUpTT04ocmVxJHBvc3RCb2R5LCBzaW1wbGlmeVZlY3RvciA9IEZBTFNFKQ0KICAjIGV4cGVjdCBlaXRoZXIgSlNPTiBvYmplY3RzIGluIGFuIGFycmF5IG9yIG5lc3RlZCBKU09OIG9iamVjdHMNCiAgbmV3ZGF0YSA9IHJiaW5kbGlzdChuZXdkYXRhLCB1c2UubmFtZXMgPSBUUlVFKQ0KICAjIGNvbnZlcnQgYWxsIGZlYXR1cmVzIGluIHBsYWNlIHRvIHRoZWlyIGV4cGVjdGVkIGZlYXR1cmVfdHlwZQ0KICBuZXdkYXRhWywgY29sbmFtZXMobmV3ZGF0YSkgOj0gbWxyM21pc2M6OnBtYXAobGlzdCguU0QsIGNvbG5hbWVzKG5ld2RhdGEpKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGZpeF9mZWF0dXJlX3R5cGVzLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZmVhdHVyZV9pbmZvID0gZmVhdHVyZV9pbmZvKV0NCiAgIyBwcmVkaWN0IGFuZCByZXR1cm4gYXMgYSBkYXRhLnRhYmxlDQogIHByZWQgPSBhcy5kYXRhLnRhYmxlKGdsJHByZWRpY3RfbmV3ZGF0YShuZXdkYXRhKSkNCiAgc2NvcmUgPSAxMDAwIC0gcHJlZCRwcm9iLmJhZCAqIDEwMDANCiAgcmV0dXJuKHNjb3JlKQ0KICAjIG9yIG9ubHkgdGhlIG51bWVyaWMgdmFsdWVzDQogICMgZ2wkcHJlZGljdF9uZXdkYXRhKG5ld2RhdGEpJHJlc3BvbnNlDQp9DQpgYGANCg0KIyMgVOG6oW8gQVBJIA0KDQpT4butIGThu6VuZyBwYWNrYWdlIHBsdW1iZXIgY2hvIMSR4buDIGPDoGkgxJHhurd0IHdlYiBzZXJ2aWNlLg0KVHJvbmcgcGjhuqduIG7DoHkgc+G6vSBjaOG6oXkgcGjhuqduIGNvZGUgxJHhu4MgZOG7sSBiw6FvIChwcmVkaWN0X2dsLlIpIHbDoCBraGFpIGLDoW8gaG9zdCwgcG9ydC4NCg0KYGBge3IsIGV2YWw9RkFMU0V9DQpsaWJyYXJ5KHBsdW1iZXIpDQpyID0gcGx1bWIoZmlsZSA9ICJSL3ByZWRpY3RfZ2wuUiIsIGRpciA9J1IvbW9kZWxfZGVwbG95JykNCnIkcnVuKHBvcnQgPSAxMDMwLCBob3N0ID0gIjAuMC4wLjAiKQ0KYGBgDQoNCiMjIEfhu61pIHJlcXVlc3QgYm9keQ0KDQpWw60gZOG7pSAxIHRyxrDhu51uZyBo4bujcCBn4butaSBk4buvIGxp4buHdSDEkeG6v24gbcOheSBjaOG7pyB2w6Aga+G6v3QgcXXhuqMgbmjhuq1uIMSRxrDhu6NjLg0KYGBge3IsIGV2YWw9RkFMU0V9DQpuZXdkYXRhID0gJ1t7ImFtb3VudCI6MTE2OSwgInB1cnBvc2UiOiJyZXBhaXJzIiwgImFnZSI6IjIwIn1dJw0KcmVzcCA9IGh0dHI6OlBPU1QodXJsID0gImh0dHA6Ly8xMjcuMC4wLjE6MTAzMC9wcmVkaWN0X2NyZWRpdF9yaXNrIiwNCiAgICAgICAgICAgICAgICAgIGJvZHkgPSBuZXdkYXRhLCBlbmNvZGUgPSAianNvbiIpDQpodHRyOjpjb250ZW50KHJlc3ApDQpgYGANCg0KDQoNCg==