Motivations

Tinh chỉnh tham số để tìm tham số tối ưu cho mô hình là công việc tốn thời gian và nặng nhọc. Bayesian Optimization là một cách tiếp cận hiệu quả để tinh chỉnh tham số cho các mô hình Machine Learning.

Data used and results

Trước hết train và đánh giá một loạt Machine Learning Classifiers. Kết quả cho thấy XGBoost có trung bình Recall (n_splits=3, n_repeats=4) là 0.7176:

# Hide warnings from Python: 

import warnings
warnings.simplefilter(action="ignore", category=FutureWarning)

# Load data:
import pandas as pd
df = pd.read_csv("http://www.creditriskanalytics.net/uploads/1/9/5/1/19511601/hmeq.csv")

# Convert categories to dummies:
df = pd.get_dummies(df)

# Impute missing data (https://academic.oup.com/bioinformatics/article/28/1/112/219101,
#                      https://academic.oup.com/aje/article/179/6/764/107562,
#                      https://github.com/epsilon-machine/missingpy):

from missingpy import MissForest
imputer = MissForest()
df_imputed = imputer.fit_transform(df)

# Convert to data frame:
df_imputed = pd.DataFrame(df_imputed)

# Rename for columns:
df_imputed.columns = df.columns

# Prepare data:
X = df_imputed.drop(labels=["BAD"], axis=1)
Y = df_imputed["BAD"]

# Standardize 0-1 for features:
from sklearn.preprocessing import MinMaxScaler
scaler_01 = MinMaxScaler()
scaler_01.fit(X)
X_scaler = scaler_01.transform(X)

# Some classifiers from Scikit-learn:
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.ensemble import ExtraTreesClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import BaggingClassifier
from sklearn.neural_network import MLPClassifier

# LightGBM, Catboost and XGBoost:
from lightgbm import LGBMClassifier
from xgboost import XGBClassifier
from catboost import CatBoostClassifier

# Initiative estimators:
ran = RandomForestClassifier()
gbm = LGBMClassifier()
log = LogisticRegression()
gbc = GradientBoostingClassifier()
xgb = XGBClassifier()
ext = ExtraTreesClassifier()
ada = AdaBoostClassifier()
gnb = GaussianNB()
bag = BaggingClassifier()
nnn = MLPClassifier()
cat = CatBoostClassifier()

# List of classifiers:
models = [ran, gbm, log, gbc, xgb, ext, ada, gnb, bag, nnn, cat]

# Train all classifiers by using for loop:
from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.model_selection import cross_val_score
cv = RepeatedStratifiedKFold(n_splits=3, n_repeats=4, random_state=29)

recall_mean = []
recall_sd = []
auc_mean = []
auc_sd = []

import numpy as np

for mod in models:
    acc = cross_val_score(mod, X_scaler, Y, scoring="recall", cv=cv, n_jobs=-1, verbose=False)
    auc = cross_val_score(mod, X_scaler, Y, scoring="roc_auc", cv=cv, n_jobs=-1, verbose=False)
    # Recall metric:
    recall_mean.append(acc.mean())
    recall_sd.append(np.std(acc))
    # AUC metric:
    auc_mean.append(auc.mean())
    auc_sd.append(np.std(auc))

df_results = pd.DataFrame({"Classifier": [j.__class__.__name__ for j in models],
                           "Recall_mean": recall_mean,
                           "Recall_sd": recall_sd,
                           "AUC_mean": auc_mean,
                           "AUC_sd": auc_sd})

# Report results:
print(df_results)

Chúng ta có thể tinh chỉnh tham số theo Bayesian Optimization để cho Recall tăng lên 0.7400 (mức tăng 3% - khá là khiêm tốn) như sau:

# Define the space of parameters to search:
from skopt.space import Integer
from skopt.space import Real
from skopt.space import Categorical
from skopt.utils import use_named_args
from skopt import gp_minimize

# Space of parameters:
search_space = list()
search_space.append(Categorical(["binary:logistic"], name="objective"))
search_space.append(Integer(2, 20, name="max_depth"))
search_space.append(Integer(2, 20, name="min_child_weight"))
search_space.append(Integer(10, 1000, name="n_estimators"))
search_space.append(Real(1e-3, 100, "log-uniform", name="learning_rate"))
search_space.append(Real(1e-2, 100, "log-uniform", name="eta"))
search_space.append(Real(0.05, 0.8, name="gamma"))
search_space.append(Real(0.1, 0.9, name="subsample"))
search_space.append(Real(0.5, 1, name="colsample_bytree"))

# Define the function used to evaluate a given configuration:
@use_named_args(search_space)
def evaluate_model(**params):
    model = XGBClassifier()
    model.set_params(**params)
    result = cross_val_score(model, X_scaler, Y, cv=cv, n_jobs=-1, scoring="recall")
    estimate = result.mean()
    return -1 * estimate

# Perform optimization:
result_opt = gp_minimize(func=evaluate_model, dimensions=search_space, random_state=29, verbose=True)

# Report findings:
print("Best Recall: %.3f" % (-1 * result_opt.fun))
print("Best Parameters: %s" % result_opt.x)

Huấn luyện lại XGBoost với tham số tối ưu tìm được. Recall trên test data là 0.7580:

# Best XGB:
best_param = result_opt.x
best_xgb = XGBClassifier(objective=str(best_param[0]),
                         max_depth=int(best_param[1]),
                         min_child_weight=int(best_param[2]),
                         n_estimators=int(best_param[3]),
                         learning_rate=float(best_param[4]),
                         eta=float(best_param[5]),
                         gamma=float(best_param[6]),
                         subsample=float(best_param[7]),
                         colsample_bytree=float(best_param[8]),
                         random_state=29,
                         n_jobs=-1)

# Split data:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_scaler, Y, test_size=0.3, random_state=1)

# Train best XGBClassifier:
best_xgb.fit(X_train, y_train)
pred = best_xgb.predict(X_test)

# Recall metrics:
from sklearn.metrics import recall_score
recall_score(y_test, pred)

# CM metrics:
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, pred)
print(cm)
LS0tDQp0aXRsZTogJ0JheWVzaWFuIE9wdGltaXphdGlvbiBmb3Igc2VhcmNoaW5nIG9wdGltYWwgUmVjYWxsIGZvciBYR0Jvb3N0IENsYXNzaWZpZXIgKFB5dGhvbiknDQphdXRob3I6ICdBdXRob3I6IE5ndXllbiBDaGkgRHVuZycNCnN1YnRpdGxlOiAiUHl0aG9uIE1hY2hpbmUgTGVhcm5pbmcgU2VyaWVzIg0Kb3V0cHV0Og0KICBodG1sX2RvY3VtZW50OiANCiAgICBjb2RlX2Rvd25sb2FkOiB0cnVlDQogICAgIyBjb2RlX2ZvbGRpbmc6IGhpZGUNCiAgICBoaWdobGlnaHQ6IHplbmJ1cm4NCiAgICAjIG51bWJlcl9zZWN0aW9uczogeWVzDQogICAgdGhlbWU6ICJmbGF0bHkiDQogICAgdG9jOiBUUlVFDQogICAgdG9jX2Zsb2F0OiBUUlVFDQotLS0NCg0KYGBge3Igc2V0dXAsaW5jbHVkZT1GQUxTRX0NCmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgd2FybmluZyA9IEZBTFNFLCBtZXNzYWdlID0gRkFMU0UsIGNhY2hlID0gVFJVRSwgZXZhbCA9IEZBTFNFKQ0KDQpgYGANCg0KDQojIE1vdGl2YXRpb25zDQoNClRpbmggY2jhu4luaCB0aGFtIHPhu5EgxJHhu4MgdMOsbSB0aGFtIHPhu5EgdOG7kWkgxrB1IGNobyBtw7QgaMOsbmggbMOgIGPDtG5nIHZp4buHYyB04buRbiB0aOG7nWkgZ2lhbiB2w6AgbuG6t25nIG5o4buNYy4gQmF5ZXNpYW4gT3B0aW1pemF0aW9uIGzDoCBt4buZdCBjw6FjaCB0aeG6v3AgY+G6rW4gaGnhu4d1IHF14bqjIMSR4buDIHRpbmggY2jhu4luaCB0aGFtIHPhu5EgY2hvIGPDoWMgbcO0IGjDrG5oIE1hY2hpbmUgTGVhcm5pbmcuIA0KDQoNCiMgRGF0YSB1c2VkIGFuZCByZXN1bHRzDQoNClRyxrDhu5tjIGjhur90IHRyYWluIHbDoCDEkcOhbmggZ2nDoSBt4buZdCBsb+G6oXQgTWFjaGluZSBMZWFybmluZyBDbGFzc2lmaWVycy4gS+G6v3QgcXXhuqMgY2hvIHRo4bqleSBYR0Jvb3N0IGPDsyB0cnVuZyBiw6xuaCBSZWNhbGwgKG5fc3BsaXRzPTMsIG5fcmVwZWF0cz00KSBsw6AgMC43MTc2OiANCg0KDQpgYGB7ciwgZXZhbD1GQUxTRX0NCiMgSGlkZSB3YXJuaW5ncyBmcm9tIFB5dGhvbjogDQoNCmltcG9ydCB3YXJuaW5ncw0Kd2FybmluZ3Muc2ltcGxlZmlsdGVyKGFjdGlvbj0iaWdub3JlIiwgY2F0ZWdvcnk9RnV0dXJlV2FybmluZykNCg0KIyBMb2FkIGRhdGE6DQppbXBvcnQgcGFuZGFzIGFzIHBkDQpkZiA9IHBkLnJlYWRfY3N2KCJodHRwOi8vd3d3LmNyZWRpdHJpc2thbmFseXRpY3MubmV0L3VwbG9hZHMvMS85LzUvMS8xOTUxMTYwMS9obWVxLmNzdiIpDQoNCiMgQ29udmVydCBjYXRlZ29yaWVzIHRvIGR1bW1pZXM6DQpkZiA9IHBkLmdldF9kdW1taWVzKGRmKQ0KDQojIEltcHV0ZSBtaXNzaW5nIGRhdGEgKGh0dHBzOi8vYWNhZGVtaWMub3VwLmNvbS9iaW9pbmZvcm1hdGljcy9hcnRpY2xlLzI4LzEvMTEyLzIxOTEwMSwNCiMgICAgICAgICAgICAgICAgICAgICAgaHR0cHM6Ly9hY2FkZW1pYy5vdXAuY29tL2FqZS9hcnRpY2xlLzE3OS82Lzc2NC8xMDc1NjIsDQojICAgICAgICAgICAgICAgICAgICAgIGh0dHBzOi8vZ2l0aHViLmNvbS9lcHNpbG9uLW1hY2hpbmUvbWlzc2luZ3B5KToNCg0KZnJvbSBtaXNzaW5ncHkgaW1wb3J0IE1pc3NGb3Jlc3QNCmltcHV0ZXIgPSBNaXNzRm9yZXN0KCkNCmRmX2ltcHV0ZWQgPSBpbXB1dGVyLmZpdF90cmFuc2Zvcm0oZGYpDQoNCiMgQ29udmVydCB0byBkYXRhIGZyYW1lOg0KZGZfaW1wdXRlZCA9IHBkLkRhdGFGcmFtZShkZl9pbXB1dGVkKQ0KDQojIFJlbmFtZSBmb3IgY29sdW1uczoNCmRmX2ltcHV0ZWQuY29sdW1ucyA9IGRmLmNvbHVtbnMNCg0KIyBQcmVwYXJlIGRhdGE6DQpYID0gZGZfaW1wdXRlZC5kcm9wKGxhYmVscz1bIkJBRCJdLCBheGlzPTEpDQpZID0gZGZfaW1wdXRlZFsiQkFEIl0NCg0KIyBTdGFuZGFyZGl6ZSAwLTEgZm9yIGZlYXR1cmVzOg0KZnJvbSBza2xlYXJuLnByZXByb2Nlc3NpbmcgaW1wb3J0IE1pbk1heFNjYWxlcg0Kc2NhbGVyXzAxID0gTWluTWF4U2NhbGVyKCkNCnNjYWxlcl8wMS5maXQoWCkNClhfc2NhbGVyID0gc2NhbGVyXzAxLnRyYW5zZm9ybShYKQ0KDQojIFNvbWUgY2xhc3NpZmllcnMgZnJvbSBTY2lraXQtbGVhcm46DQpmcm9tIHNrbGVhcm4uZW5zZW1ibGUgaW1wb3J0IFJhbmRvbUZvcmVzdENsYXNzaWZpZXINCmZyb20gc2tsZWFybi5saW5lYXJfbW9kZWwgaW1wb3J0IExvZ2lzdGljUmVncmVzc2lvbg0KZnJvbSBza2xlYXJuLmVuc2VtYmxlIGltcG9ydCBHcmFkaWVudEJvb3N0aW5nQ2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLmVuc2VtYmxlIGltcG9ydCBFeHRyYVRyZWVzQ2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLmVuc2VtYmxlIGltcG9ydCBBZGFCb29zdENsYXNzaWZpZXINCmZyb20gc2tsZWFybi5uYWl2ZV9iYXllcyBpbXBvcnQgR2F1c3NpYW5OQg0KZnJvbSBza2xlYXJuLmVuc2VtYmxlIGltcG9ydCBCYWdnaW5nQ2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLm5ldXJhbF9uZXR3b3JrIGltcG9ydCBNTFBDbGFzc2lmaWVyDQoNCiMgTGlnaHRHQk0sIENhdGJvb3N0IGFuZCBYR0Jvb3N0Og0KZnJvbSBsaWdodGdibSBpbXBvcnQgTEdCTUNsYXNzaWZpZXINCmZyb20geGdib29zdCBpbXBvcnQgWEdCQ2xhc3NpZmllcg0KZnJvbSBjYXRib29zdCBpbXBvcnQgQ2F0Qm9vc3RDbGFzc2lmaWVyDQoNCiMgSW5pdGlhdGl2ZSBlc3RpbWF0b3JzOg0KcmFuID0gUmFuZG9tRm9yZXN0Q2xhc3NpZmllcigpDQpnYm0gPSBMR0JNQ2xhc3NpZmllcigpDQpsb2cgPSBMb2dpc3RpY1JlZ3Jlc3Npb24oKQ0KZ2JjID0gR3JhZGllbnRCb29zdGluZ0NsYXNzaWZpZXIoKQ0KeGdiID0gWEdCQ2xhc3NpZmllcigpDQpleHQgPSBFeHRyYVRyZWVzQ2xhc3NpZmllcigpDQphZGEgPSBBZGFCb29zdENsYXNzaWZpZXIoKQ0KZ25iID0gR2F1c3NpYW5OQigpDQpiYWcgPSBCYWdnaW5nQ2xhc3NpZmllcigpDQpubm4gPSBNTFBDbGFzc2lmaWVyKCkNCmNhdCA9IENhdEJvb3N0Q2xhc3NpZmllcigpDQoNCiMgTGlzdCBvZiBjbGFzc2lmaWVyczoNCm1vZGVscyA9IFtyYW4sIGdibSwgbG9nLCBnYmMsIHhnYiwgZXh0LCBhZGEsIGduYiwgYmFnLCBubm4sIGNhdF0NCg0KIyBUcmFpbiBhbGwgY2xhc3NpZmllcnMgYnkgdXNpbmcgZm9yIGxvb3A6DQpmcm9tIHNrbGVhcm4ubW9kZWxfc2VsZWN0aW9uIGltcG9ydCBSZXBlYXRlZFN0cmF0aWZpZWRLRm9sZA0KZnJvbSBza2xlYXJuLm1vZGVsX3NlbGVjdGlvbiBpbXBvcnQgY3Jvc3NfdmFsX3Njb3JlDQpjdiA9IFJlcGVhdGVkU3RyYXRpZmllZEtGb2xkKG5fc3BsaXRzPTMsIG5fcmVwZWF0cz00LCByYW5kb21fc3RhdGU9MjkpDQoNCnJlY2FsbF9tZWFuID0gW10NCnJlY2FsbF9zZCA9IFtdDQphdWNfbWVhbiA9IFtdDQphdWNfc2QgPSBbXQ0KDQppbXBvcnQgbnVtcHkgYXMgbnANCg0KZm9yIG1vZCBpbiBtb2RlbHM6DQogICAgYWNjID0gY3Jvc3NfdmFsX3Njb3JlKG1vZCwgWF9zY2FsZXIsIFksIHNjb3Jpbmc9InJlY2FsbCIsIGN2PWN2LCBuX2pvYnM9LTEsIHZlcmJvc2U9RmFsc2UpDQogICAgYXVjID0gY3Jvc3NfdmFsX3Njb3JlKG1vZCwgWF9zY2FsZXIsIFksIHNjb3Jpbmc9InJvY19hdWMiLCBjdj1jdiwgbl9qb2JzPS0xLCB2ZXJib3NlPUZhbHNlKQ0KICAgICMgUmVjYWxsIG1ldHJpYzoNCiAgICByZWNhbGxfbWVhbi5hcHBlbmQoYWNjLm1lYW4oKSkNCiAgICByZWNhbGxfc2QuYXBwZW5kKG5wLnN0ZChhY2MpKQ0KICAgICMgQVVDIG1ldHJpYzoNCiAgICBhdWNfbWVhbi5hcHBlbmQoYXVjLm1lYW4oKSkNCiAgICBhdWNfc2QuYXBwZW5kKG5wLnN0ZChhdWMpKQ0KDQpkZl9yZXN1bHRzID0gcGQuRGF0YUZyYW1lKHsiQ2xhc3NpZmllciI6IFtqLl9fY2xhc3NfXy5fX25hbWVfXyBmb3IgaiBpbiBtb2RlbHNdLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIlJlY2FsbF9tZWFuIjogcmVjYWxsX21lYW4sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAiUmVjYWxsX3NkIjogcmVjYWxsX3NkLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIkFVQ19tZWFuIjogYXVjX21lYW4sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAiQVVDX3NkIjogYXVjX3NkfSkNCg0KIyBSZXBvcnQgcmVzdWx0czoNCnByaW50KGRmX3Jlc3VsdHMpDQpgYGANCg0KDQpDaMO6bmcgdGEgY8OzIHRo4buDIHRpbmggY2jhu4luaCB0aGFtIHPhu5EgdGhlbyBCYXllc2lhbiBPcHRpbWl6YXRpb24gxJHhu4MgY2hvIFJlY2FsbCB0xINuZyBsw6puIDAuNzQwMCAobeG7qWMgdMSDbmcgMyUgLSBraMOhIGzDoCBraGnDqm0gdOG7kW4pIG5oxrAgc2F1OiANCg0KYGBge3J9DQoNCiMgRGVmaW5lIHRoZSBzcGFjZSBvZiBwYXJhbWV0ZXJzIHRvIHNlYXJjaDoNCmZyb20gc2tvcHQuc3BhY2UgaW1wb3J0IEludGVnZXINCmZyb20gc2tvcHQuc3BhY2UgaW1wb3J0IFJlYWwNCmZyb20gc2tvcHQuc3BhY2UgaW1wb3J0IENhdGVnb3JpY2FsDQpmcm9tIHNrb3B0LnV0aWxzIGltcG9ydCB1c2VfbmFtZWRfYXJncw0KZnJvbSBza29wdCBpbXBvcnQgZ3BfbWluaW1pemUNCg0KIyBTcGFjZSBvZiBwYXJhbWV0ZXJzOg0Kc2VhcmNoX3NwYWNlID0gbGlzdCgpDQpzZWFyY2hfc3BhY2UuYXBwZW5kKENhdGVnb3JpY2FsKFsiYmluYXJ5OmxvZ2lzdGljIl0sIG5hbWU9Im9iamVjdGl2ZSIpKQ0Kc2VhcmNoX3NwYWNlLmFwcGVuZChJbnRlZ2VyKDIsIDIwLCBuYW1lPSJtYXhfZGVwdGgiKSkNCnNlYXJjaF9zcGFjZS5hcHBlbmQoSW50ZWdlcigyLCAyMCwgbmFtZT0ibWluX2NoaWxkX3dlaWdodCIpKQ0Kc2VhcmNoX3NwYWNlLmFwcGVuZChJbnRlZ2VyKDEwLCAxMDAwLCBuYW1lPSJuX2VzdGltYXRvcnMiKSkNCnNlYXJjaF9zcGFjZS5hcHBlbmQoUmVhbCgxZS0zLCAxMDAsICJsb2ctdW5pZm9ybSIsIG5hbWU9ImxlYXJuaW5nX3JhdGUiKSkNCnNlYXJjaF9zcGFjZS5hcHBlbmQoUmVhbCgxZS0yLCAxMDAsICJsb2ctdW5pZm9ybSIsIG5hbWU9ImV0YSIpKQ0Kc2VhcmNoX3NwYWNlLmFwcGVuZChSZWFsKDAuMDUsIDAuOCwgbmFtZT0iZ2FtbWEiKSkNCnNlYXJjaF9zcGFjZS5hcHBlbmQoUmVhbCgwLjEsIDAuOSwgbmFtZT0ic3Vic2FtcGxlIikpDQpzZWFyY2hfc3BhY2UuYXBwZW5kKFJlYWwoMC41LCAxLCBuYW1lPSJjb2xzYW1wbGVfYnl0cmVlIikpDQoNCiMgRGVmaW5lIHRoZSBmdW5jdGlvbiB1c2VkIHRvIGV2YWx1YXRlIGEgZ2l2ZW4gY29uZmlndXJhdGlvbjoNCkB1c2VfbmFtZWRfYXJncyhzZWFyY2hfc3BhY2UpDQpkZWYgZXZhbHVhdGVfbW9kZWwoKipwYXJhbXMpOg0KICAgIG1vZGVsID0gWEdCQ2xhc3NpZmllcigpDQogICAgbW9kZWwuc2V0X3BhcmFtcygqKnBhcmFtcykNCiAgICByZXN1bHQgPSBjcm9zc192YWxfc2NvcmUobW9kZWwsIFhfc2NhbGVyLCBZLCBjdj1jdiwgbl9qb2JzPS0xLCBzY29yaW5nPSJyZWNhbGwiKQ0KICAgIGVzdGltYXRlID0gcmVzdWx0Lm1lYW4oKQ0KICAgIHJldHVybiAtMSAqIGVzdGltYXRlDQoNCiMgUGVyZm9ybSBvcHRpbWl6YXRpb246DQpyZXN1bHRfb3B0ID0gZ3BfbWluaW1pemUoZnVuYz1ldmFsdWF0ZV9tb2RlbCwgZGltZW5zaW9ucz1zZWFyY2hfc3BhY2UsIHJhbmRvbV9zdGF0ZT0yOSwgdmVyYm9zZT1UcnVlKQ0KDQojIFJlcG9ydCBmaW5kaW5nczoNCnByaW50KCJCZXN0IFJlY2FsbDogJS4zZiIgJSAoLTEgKiByZXN1bHRfb3B0LmZ1bikpDQpwcmludCgiQmVzdCBQYXJhbWV0ZXJzOiAlcyIgJSByZXN1bHRfb3B0LngpDQpgYGANCg0KSHXhuqVuIGx1eeG7h24gbOG6oWkgWEdCb29zdCB24bubaSB0aGFtIHPhu5EgdOG7kWkgxrB1IHTDrG0gxJHGsOG7o2MuIFJlY2FsbCB0csOqbiB0ZXN0IGRhdGEgbMOgIDAuNzU4MDogDQoNCg0KYGBge3J9DQojIEJlc3QgWEdCOg0KYmVzdF9wYXJhbSA9IHJlc3VsdF9vcHQueA0KYmVzdF94Z2IgPSBYR0JDbGFzc2lmaWVyKG9iamVjdGl2ZT1zdHIoYmVzdF9wYXJhbVswXSksDQogICAgICAgICAgICAgICAgICAgICAgICAgbWF4X2RlcHRoPWludChiZXN0X3BhcmFtWzFdKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBtaW5fY2hpbGRfd2VpZ2h0PWludChiZXN0X3BhcmFtWzJdKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBuX2VzdGltYXRvcnM9aW50KGJlc3RfcGFyYW1bM10pLA0KICAgICAgICAgICAgICAgICAgICAgICAgIGxlYXJuaW5nX3JhdGU9ZmxvYXQoYmVzdF9wYXJhbVs0XSksDQogICAgICAgICAgICAgICAgICAgICAgICAgZXRhPWZsb2F0KGJlc3RfcGFyYW1bNV0pLA0KICAgICAgICAgICAgICAgICAgICAgICAgIGdhbW1hPWZsb2F0KGJlc3RfcGFyYW1bNl0pLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHN1YnNhbXBsZT1mbG9hdChiZXN0X3BhcmFtWzddKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBjb2xzYW1wbGVfYnl0cmVlPWZsb2F0KGJlc3RfcGFyYW1bOF0pLA0KICAgICAgICAgICAgICAgICAgICAgICAgIHJhbmRvbV9zdGF0ZT0yOSwNCiAgICAgICAgICAgICAgICAgICAgICAgICBuX2pvYnM9LTEpDQoNCiMgU3BsaXQgZGF0YToNCmZyb20gc2tsZWFybi5tb2RlbF9zZWxlY3Rpb24gaW1wb3J0IHRyYWluX3Rlc3Rfc3BsaXQNClhfdHJhaW4sIFhfdGVzdCwgeV90cmFpbiwgeV90ZXN0ID0gdHJhaW5fdGVzdF9zcGxpdChYX3NjYWxlciwgWSwgdGVzdF9zaXplPTAuMywgcmFuZG9tX3N0YXRlPTEpDQoNCiMgVHJhaW4gYmVzdCBYR0JDbGFzc2lmaWVyOg0KYmVzdF94Z2IuZml0KFhfdHJhaW4sIHlfdHJhaW4pDQpwcmVkID0gYmVzdF94Z2IucHJlZGljdChYX3Rlc3QpDQoNCiMgUmVjYWxsIG1ldHJpY3M6DQpmcm9tIHNrbGVhcm4ubWV0cmljcyBpbXBvcnQgcmVjYWxsX3Njb3JlDQpyZWNhbGxfc2NvcmUoeV90ZXN0LCBwcmVkKQ0KDQojIENNIG1ldHJpY3M6DQpmcm9tIHNrbGVhcm4ubWV0cmljcyBpbXBvcnQgY29uZnVzaW9uX21hdHJpeA0KY20gPSBjb25mdXNpb25fbWF0cml4KHlfdGVzdCwgcHJlZCkNCnByaW50KGNtKQ0KYGBgDQoNCg0KDQo=