Motivations
Hầu hết các thuật toán học máy đều hướng đến tối ưu một tiêu chuẩn kiểu như ROC-AUC, Recall. Những tiêu chuẩn này có thể chưa phù hợp với mục tiêu mà hầu hết các tổ chức hoạt động vì lợi nhuận theo đuổi là Lợi Nhuận. Câu hỏi ở đây là quá trình tối ưu hóa các tham số (Hyperparameter Optimization) - là một quá trình tốn kém thời gian, công sức và tiền bạc có phù hợp với mục tiêu tối đa hóa lợi nhuận hay không?
Findings
Câu trả lời có thể được nhìn thấy ở Figure 1 dưới đây:

Câu trả lời là rõ ràng: maximum profit của Random Forest sử dụng các tham số tối ưu cao hơn maximum profit của Random Forest mặc định không tinh chỉnh tham số khoảng 82%.
Python Codes
Thực nghiệm để có những kết luận trên được thực hiện bằng Python với bộ dữ liệu GermanCredit.csv (có thể download bộ dữ liệu này tại đây):
# =================================
# Prepare data
# =================================
# Load data and conduct data pre-processing:
import pandas as pd
df_bank = pd.read_csv("C:/Users/ADMIN/Desktop/DataMining/dmba/GermanCredit.csv")
df_bank["RESPONSE"] = df_bank["RESPONSE"].map({1: 0, 0: 1})
# Drop OBS# feature:
my_df_binary = df_bank.drop(["OBS#"], axis=1)
# Define input features and target output:
Y = my_df_binary["RESPONSE"]
X = my_df_binary.drop("RESPONSE", axis=1)
# Prepare data:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=29)
# ==========================================================================
# Search optimal parameters for Random Forest using Bayesian Optimization
# ==========================================================================
# Define objective function:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.model_selection import RepeatedStratifiedKFold
cv = RepeatedStratifiedKFold(n_splits=4, n_repeats=3, random_state=29)
def objective_function(params):
clf = RandomForestClassifier(**params, n_jobs=-1, random_state=29)
score = cross_val_score(clf, X_train, y_train, cv=cv, scoring="roc_auc", n_jobs=-1)
loss_value = -1 * score.mean()
return loss_value
# Define space of parameters:
from hyperopt.pyll import scope
from hyperopt import hp
param_hyperopt_rf = {
'max_depth': scope.int(hp.quniform('max_depth', 1, 50, 1)),
'n_estimators': scope.int(hp.quniform('n_estimators', 50, 1000, 100)),
'min_samples_split': scope.int(hp.quniform('min_samples_split', 2, 30, 1)),
'min_samples_leaf': scope.int(hp.quniform('min_samples_leaf', 2, 30, 1))
}
# Search optimal parameters for Random Forest by Bayesian Optimization:
from hyperopt import fmin, tpe, Trials
import numpy as np
tpe = tpe.suggest
tpe_trials = Trials()
rf_bayesian_TPE = fmin(fn=objective_function,
space=param_hyperopt_rf,
algo=tpe,
max_evals=30,
trials=tpe_trials,
rstate=np.random.RandomState(29))
# Extract optimal values and parameter names:
best_param_tpe = [x for x in rf_bayesian_TPE.values()]
param_names = [x for x in rf_bayesian_TPE.keys()]
# Reset Random Forest with optimal parameters:
param_hyperopt_rf['max_depth'] = int(best_param_tpe[0])
param_hyperopt_rf['min_samples_leaf'] = int(best_param_tpe[1])
param_hyperopt_rf['min_samples_split'] = int(best_param_tpe[2])
param_hyperopt_rf['n_estimators'] = int(best_param_tpe[3])
# ================================================================
# Compare profit between default and turned Random Forest
# ================================================================
# Function calculates profit with given cutoff when interest rate of 10%:
def profit_by_cutoff(cutoff, pred_prob):
rate = 0.10
pred_bg = (pred_prob >= cutoff).astype(int)
gg = X_test[(y_test == 0) & (pred_bg == 0)]
bg = X_test[(y_test == 1) & (pred_bg == 0)]
profit = np.sum(rate * gg["AMOUNT"]) - np.sum(bg["AMOUNT"])
return profit
# Function calculates average profit with given cutoff:
def average_pro(cutoff):
n_times = 10
randomSeeds = np.arange(1, n_times + 1, 1)
pro1 = []
pro2 = []
for j in randomSeeds:
# For Default RF:
rf1 = RandomForestClassifier(random_state=j, n_jobs=-1)
rf1.fit(X_train, y_train)
pd1 = rf1.predict_proba(X_test)[:, 1]
profit1 = profit_by_cutoff(cutoff=cutoff, pred_prob=pd1)
pro1.append(profit1)
# For turned RF:
rf2 = RandomForestClassifier(**param_hyperopt_rf, random_state=j, n_jobs=-1)
rf2.fit(X_train, y_train)
pd2 = rf2.predict_proba(X_test)[:, 1]
profit2 = profit_by_cutoff(cutoff=cutoff, pred_prob=pd2)
pro2.append(profit2)
df_result = pd.DataFrame({"AvgProDef": [np.mean(pro1)],
"AvgProTur": [np.mean(pro2)],
"Cutoff": [cutoff]})
return df_result
# Avg profit by range of cutoff:
df_avgPro = pd.DataFrame()
cutoff_range = np.arange(0.01, 0.3, 0.005)
for i in cutoff_range:
df_i = average_pro(cutoff=i)
df_avgPro = df_avgPro.append(df_i)
# Result:
import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')
plt.figure(figsize=(8, 6))
plt.plot("Cutoff", "AvgProDef", data=df_avgPro, label="Default RF", lw=2)
plt.plot("Cutoff", "AvgProTur", data=df_avgPro, label="Turned RF", lw=2)
plt.title("Figure 1: Profit between Default and Turned RF", fontsize=13)
plt.xlabel("Cutoff")
plt.ylabel("Profit")
plt.yticks(fontsize=12)
plt.xticks(fontsize=12)
plt.legend(fontsize=8)
plt.show()
LS0tDQp0aXRsZTogJ0VmZmVjdCBvZiBIeXBlcnBhcmFtZXRlciBPcHRpbWl6YXRpb24gb24gUHJvZml0IChQeXRob24pJw0KYXV0aG9yOiAnQXV0aG9yOiBOZ3V5ZW4gQ2hpIER1bmcnDQpzdWJ0aXRsZTogIlB5dGhvbiBNYWNoaW5lIExlYXJuaW5nIFNlcmllcyINCm91dHB1dDoNCiAgaHRtbF9kb2N1bWVudDogDQogICAgY29kZV9kb3dubG9hZDogdHJ1ZQ0KICAgICMgY29kZV9mb2xkaW5nOiBoaWRlDQogICAgaGlnaGxpZ2h0OiB6ZW5idXJuDQogICAgIyBudW1iZXJfc2VjdGlvbnM6IHllcw0KICAgIHRoZW1lOiAiZmxhdGx5Ig0KICAgIHRvYzogVFJVRQ0KICAgIHRvY19mbG9hdDogVFJVRQ0KLS0tDQoNCmBgYHtyIHNldHVwLGluY2x1ZGU9RkFMU0V9DQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIHdhcm5pbmcgPSBGQUxTRSwgbWVzc2FnZSA9IEZBTFNFLCBjYWNoZSA9IFRSVUUsIGV2YWwgPSBGQUxTRSkNCg0KYGBgDQoNCg0KDQojIE1vdGl2YXRpb25zDQoNCkjhuqd1IGjhur90IGPDoWMgdGh14bqtdCB0b8OhbiBo4buNYyBtw6F5IMSR4buBdSBoxrDhu5tuZyDEkeG6v24gdOG7kWkgxrB1IG3hu5l0IHRpw6p1IGNodeG6qW4ga2nhu4N1IG5oxrAgUk9DLUFVQywgUmVjYWxsLiBOaOG7r25nIHRpw6p1IGNodeG6qW4gbsOgeSBjw7MgdGjhu4MgY2jGsGEgcGjDuSBo4bujcCB24bubaSBt4bulYyB0acOqdSBtw6AgaOG6p3UgaOG6v3QgY8OhYyB04buVIGNo4bupYyBob+G6oXQgxJHhu5luZyB2w6wgbOG7o2kgbmh14bqtbiB0aGVvIMSRdeG7lWkgbMOgIEzhu6NpIE5odeG6rW4uIEPDonUgaOG7j2kg4bufIMSRw6J5IGzDoCBxdcOhIHRyw6xuaCB04buRaSDGsHUgaMOzYSBjw6FjIHRoYW0gc+G7kSAoSHlwZXJwYXJhbWV0ZXIgT3B0aW1pemF0aW9uKSAtIGzDoCBt4buZdCBxdcOhIHRyw6xuaCB04buRbiBrw6ltIHRo4budaSBnaWFuLCBjw7RuZyBz4bupYyB2w6AgdGnhu4FuIGLhuqFjIGPDsyBwaMO5IGjhu6NwIHbhu5tpIG3hu6VjIHRpw6p1IHThu5FpIMSRYSBow7NhIGzhu6NpIG5odeG6rW4gaGF5IGtow7RuZz8gDQoNCiMgRmluZGluZ3MNCg0KQ8OidSB0cuG6oyBs4budaSBjw7MgdGjhu4MgxJHGsOG7o2MgbmjDrG4gdGjhuqV5IOG7nyBGaWd1cmUgMSBkxrDhu5tpIMSRw6J5OiANCg0KIVtdKEM6L1VzZXJzL0FETUlOL0RvY3VtZW50cy9wcm8xLmpwZykNCg0KQ8OidSB0cuG6oyBs4budaSBsw6AgcsO1IHLDoG5nOiBtYXhpbXVtIHByb2ZpdCBj4bunYSBSYW5kb20gRm9yZXN0IHPhu60gZOG7pW5nIGPDoWMgdGhhbSBz4buRIHThu5FpIMawdSBjYW8gaMahbiBtYXhpbXVtIHByb2ZpdCBj4bunYSBSYW5kb20gRm9yZXN0IG3hurdjIMSR4buLbmgga2jDtG5nIHRpbmggY2jhu4luaCB0aGFtIHPhu5Ega2hv4bqjbmcgODIlLiANCg0KIyBQeXRob24gQ29kZXMNCg0KVGjhu7FjIG5naGnhu4dtIMSR4buDIGPDsyBuaOG7r25nIGvhur90IGx14bqtbiB0csOqbiDEkcaw4bujYyB0aOG7sWMgaGnhu4duIGLhurFuZyBQeXRob24gduG7m2kgYuG7mSBk4buvIGxp4buHdSAqKkdlcm1hbkNyZWRpdC5jc3YqKiAoY8OzIHRo4buDIGRvd25sb2FkIGLhu5kgZOG7ryBsaeG7h3UgbsOgeSBbdOG6oWkgxJHDonldKGh0dHBzOi8vd3d3LmRhdGFtaW5pbmdib29rLmNvbS9ib29rL3ItZWRpdGlvbikpOiANCg0KDQpgYGB7cn0NCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojICAgICAgICBQcmVwYXJlIGRhdGENCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojIExvYWQgZGF0YSBhbmQgY29uZHVjdCBkYXRhIHByZS1wcm9jZXNzaW5nOg0KaW1wb3J0IHBhbmRhcyBhcyBwZA0KDQpkZl9iYW5rID0gcGQucmVhZF9jc3YoIkM6L1VzZXJzL0FETUlOL0Rlc2t0b3AvRGF0YU1pbmluZy9kbWJhL0dlcm1hbkNyZWRpdC5jc3YiKQ0KZGZfYmFua1siUkVTUE9OU0UiXSA9IGRmX2JhbmtbIlJFU1BPTlNFIl0ubWFwKHsxOiAwLCAwOiAxfSkNCg0KIyBEcm9wIE9CUyMgZmVhdHVyZToNCm15X2RmX2JpbmFyeSA9IGRmX2JhbmsuZHJvcChbIk9CUyMiXSwgYXhpcz0xKQ0KDQojIERlZmluZSBpbnB1dCBmZWF0dXJlcyBhbmQgdGFyZ2V0IG91dHB1dDoNClkgPSBteV9kZl9iaW5hcnlbIlJFU1BPTlNFIl0NClggPSBteV9kZl9iaW5hcnkuZHJvcCgiUkVTUE9OU0UiLCBheGlzPTEpDQoNCiMgUHJlcGFyZSBkYXRhOg0KZnJvbSBza2xlYXJuLm1vZGVsX3NlbGVjdGlvbiBpbXBvcnQgdHJhaW5fdGVzdF9zcGxpdA0KDQpYX3RyYWluLCBYX3Rlc3QsIHlfdHJhaW4sIHlfdGVzdCA9IHRyYWluX3Rlc3Rfc3BsaXQoWCwgWSwgdGVzdF9zaXplPTAuMiwgcmFuZG9tX3N0YXRlPTI5KQ0KDQojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQojIFNlYXJjaCBvcHRpbWFsIHBhcmFtZXRlcnMgZm9yIFJhbmRvbSBGb3Jlc3QgdXNpbmcgQmF5ZXNpYW4gT3B0aW1pemF0aW9uDQojID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCiMgRGVmaW5lIG9iamVjdGl2ZSBmdW5jdGlvbjoNCmZyb20gc2tsZWFybi5lbnNlbWJsZSBpbXBvcnQgUmFuZG9tRm9yZXN0Q2xhc3NpZmllcg0KZnJvbSBza2xlYXJuLm1vZGVsX3NlbGVjdGlvbiBpbXBvcnQgY3Jvc3NfdmFsX3Njb3JlDQpmcm9tIHNrbGVhcm4ubW9kZWxfc2VsZWN0aW9uIGltcG9ydCBSZXBlYXRlZFN0cmF0aWZpZWRLRm9sZA0KDQpjdiA9IFJlcGVhdGVkU3RyYXRpZmllZEtGb2xkKG5fc3BsaXRzPTQsIG5fcmVwZWF0cz0zLCByYW5kb21fc3RhdGU9MjkpDQoNCmRlZiBvYmplY3RpdmVfZnVuY3Rpb24ocGFyYW1zKToNCiAgICBjbGYgPSBSYW5kb21Gb3Jlc3RDbGFzc2lmaWVyKCoqcGFyYW1zLCBuX2pvYnM9LTEsIHJhbmRvbV9zdGF0ZT0yOSkNCiAgICBzY29yZSA9IGNyb3NzX3ZhbF9zY29yZShjbGYsIFhfdHJhaW4sIHlfdHJhaW4sIGN2PWN2LCBzY29yaW5nPSJyb2NfYXVjIiwgbl9qb2JzPS0xKQ0KICAgIGxvc3NfdmFsdWUgPSAtMSAqIHNjb3JlLm1lYW4oKQ0KICAgIHJldHVybiBsb3NzX3ZhbHVlDQoNCiMgRGVmaW5lIHNwYWNlIG9mIHBhcmFtZXRlcnM6DQoNCmZyb20gaHlwZXJvcHQucHlsbCBpbXBvcnQgc2NvcGUNCmZyb20gaHlwZXJvcHQgaW1wb3J0IGhwDQoNCnBhcmFtX2h5cGVyb3B0X3JmID0gew0KICAgICdtYXhfZGVwdGgnOiBzY29wZS5pbnQoaHAucXVuaWZvcm0oJ21heF9kZXB0aCcsIDEsIDUwLCAxKSksDQogICAgJ25fZXN0aW1hdG9ycyc6IHNjb3BlLmludChocC5xdW5pZm9ybSgnbl9lc3RpbWF0b3JzJywgNTAsIDEwMDAsIDEwMCkpLA0KICAgICdtaW5fc2FtcGxlc19zcGxpdCc6IHNjb3BlLmludChocC5xdW5pZm9ybSgnbWluX3NhbXBsZXNfc3BsaXQnLCAyLCAzMCwgMSkpLA0KICAgICdtaW5fc2FtcGxlc19sZWFmJzogc2NvcGUuaW50KGhwLnF1bmlmb3JtKCdtaW5fc2FtcGxlc19sZWFmJywgMiwgMzAsIDEpKQ0KfQ0KDQojIFNlYXJjaCBvcHRpbWFsIHBhcmFtZXRlcnMgZm9yIFJhbmRvbSBGb3Jlc3QgYnkgQmF5ZXNpYW4gT3B0aW1pemF0aW9uOg0KZnJvbSBoeXBlcm9wdCBpbXBvcnQgZm1pbiwgdHBlLCBUcmlhbHMNCmltcG9ydCBudW1weSBhcyBucA0KDQp0cGUgPSB0cGUuc3VnZ2VzdA0KdHBlX3RyaWFscyA9IFRyaWFscygpDQoNCnJmX2JheWVzaWFuX1RQRSA9IGZtaW4oZm49b2JqZWN0aXZlX2Z1bmN0aW9uLA0KICAgICAgICAgICAgICAgICAgICAgICBzcGFjZT1wYXJhbV9oeXBlcm9wdF9yZiwNCiAgICAgICAgICAgICAgICAgICAgICAgYWxnbz10cGUsDQogICAgICAgICAgICAgICAgICAgICAgIG1heF9ldmFscz0zMCwNCiAgICAgICAgICAgICAgICAgICAgICAgdHJpYWxzPXRwZV90cmlhbHMsDQogICAgICAgICAgICAgICAgICAgICAgIHJzdGF0ZT1ucC5yYW5kb20uUmFuZG9tU3RhdGUoMjkpKQ0KDQojIEV4dHJhY3Qgb3B0aW1hbCB2YWx1ZXMgYW5kIHBhcmFtZXRlciBuYW1lczoNCmJlc3RfcGFyYW1fdHBlID0gW3ggZm9yIHggaW4gcmZfYmF5ZXNpYW5fVFBFLnZhbHVlcygpXQ0KcGFyYW1fbmFtZXMgPSBbeCBmb3IgeCBpbiByZl9iYXllc2lhbl9UUEUua2V5cygpXQ0KDQojIFJlc2V0IFJhbmRvbSBGb3Jlc3Qgd2l0aCBvcHRpbWFsIHBhcmFtZXRlcnM6DQpwYXJhbV9oeXBlcm9wdF9yZlsnbWF4X2RlcHRoJ10gPSBpbnQoYmVzdF9wYXJhbV90cGVbMF0pDQpwYXJhbV9oeXBlcm9wdF9yZlsnbWluX3NhbXBsZXNfbGVhZiddID0gaW50KGJlc3RfcGFyYW1fdHBlWzFdKQ0KcGFyYW1faHlwZXJvcHRfcmZbJ21pbl9zYW1wbGVzX3NwbGl0J10gPSBpbnQoYmVzdF9wYXJhbV90cGVbMl0pDQpwYXJhbV9oeXBlcm9wdF9yZlsnbl9lc3RpbWF0b3JzJ10gPSBpbnQoYmVzdF9wYXJhbV90cGVbM10pDQoNCiMgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQ0KIyAgQ29tcGFyZSBwcm9maXQgYmV0d2VlbiBkZWZhdWx0IGFuZCB0dXJuZWQgUmFuZG9tIEZvcmVzdA0KIyA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09DQoNCiMgRnVuY3Rpb24gY2FsY3VsYXRlcyBwcm9maXQgd2l0aCBnaXZlbiBjdXRvZmYgd2hlbiBpbnRlcmVzdCByYXRlIG9mIDEwJToNCmRlZiBwcm9maXRfYnlfY3V0b2ZmKGN1dG9mZiwgcHJlZF9wcm9iKToNCiAgICByYXRlID0gMC4xMA0KICAgIHByZWRfYmcgPSAocHJlZF9wcm9iID49IGN1dG9mZikuYXN0eXBlKGludCkNCiAgICBnZyA9IFhfdGVzdFsoeV90ZXN0ID09IDApICYgKHByZWRfYmcgPT0gMCldDQogICAgYmcgPSBYX3Rlc3RbKHlfdGVzdCA9PSAxKSAmIChwcmVkX2JnID09IDApXQ0KICAgIHByb2ZpdCA9IG5wLnN1bShyYXRlICogZ2dbIkFNT1VOVCJdKSAtIG5wLnN1bShiZ1siQU1PVU5UIl0pDQogICAgcmV0dXJuIHByb2ZpdA0KDQojIEZ1bmN0aW9uIGNhbGN1bGF0ZXMgYXZlcmFnZSBwcm9maXQgd2l0aCBnaXZlbiBjdXRvZmY6DQoNCmRlZiBhdmVyYWdlX3BybyhjdXRvZmYpOg0KICAgIG5fdGltZXMgPSAxMA0KICAgIHJhbmRvbVNlZWRzID0gbnAuYXJhbmdlKDEsIG5fdGltZXMgKyAxLCAxKQ0KDQogICAgcHJvMSA9IFtdDQogICAgcHJvMiA9IFtdDQoNCiAgICBmb3IgaiBpbiByYW5kb21TZWVkczoNCiAgICAgICAgIyBGb3IgRGVmYXVsdCBSRjoNCiAgICAgICAgcmYxID0gUmFuZG9tRm9yZXN0Q2xhc3NpZmllcihyYW5kb21fc3RhdGU9aiwgbl9qb2JzPS0xKQ0KICAgICAgICByZjEuZml0KFhfdHJhaW4sIHlfdHJhaW4pDQogICAgICAgIHBkMSA9IHJmMS5wcmVkaWN0X3Byb2JhKFhfdGVzdClbOiwgMV0NCiAgICAgICAgcHJvZml0MSA9IHByb2ZpdF9ieV9jdXRvZmYoY3V0b2ZmPWN1dG9mZiwgcHJlZF9wcm9iPXBkMSkNCiAgICAgICAgcHJvMS5hcHBlbmQocHJvZml0MSkNCiAgICAgICAgIyBGb3IgdHVybmVkIFJGOg0KICAgICAgICByZjIgPSBSYW5kb21Gb3Jlc3RDbGFzc2lmaWVyKCoqcGFyYW1faHlwZXJvcHRfcmYsIHJhbmRvbV9zdGF0ZT1qLCBuX2pvYnM9LTEpDQogICAgICAgIHJmMi5maXQoWF90cmFpbiwgeV90cmFpbikNCiAgICAgICAgcGQyID0gcmYyLnByZWRpY3RfcHJvYmEoWF90ZXN0KVs6LCAxXQ0KICAgICAgICBwcm9maXQyID0gcHJvZml0X2J5X2N1dG9mZihjdXRvZmY9Y3V0b2ZmLCBwcmVkX3Byb2I9cGQyKQ0KICAgICAgICBwcm8yLmFwcGVuZChwcm9maXQyKQ0KDQogICAgZGZfcmVzdWx0ID0gcGQuRGF0YUZyYW1lKHsiQXZnUHJvRGVmIjogW25wLm1lYW4ocHJvMSldLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIkF2Z1Byb1R1ciI6IFtucC5tZWFuKHBybzIpXSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJDdXRvZmYiOiBbY3V0b2ZmXX0pDQoNCiAgICByZXR1cm4gZGZfcmVzdWx0DQoNCiMgQXZnIHByb2ZpdCBieSByYW5nZSBvZiBjdXRvZmY6DQpkZl9hdmdQcm8gPSBwZC5EYXRhRnJhbWUoKQ0KY3V0b2ZmX3JhbmdlID0gbnAuYXJhbmdlKDAuMDEsIDAuMywgMC4wMDUpDQoNCmZvciBpIGluIGN1dG9mZl9yYW5nZToNCiAgICBkZl9pID0gYXZlcmFnZV9wcm8oY3V0b2ZmPWkpDQogICAgZGZfYXZnUHJvID0gZGZfYXZnUHJvLmFwcGVuZChkZl9pKQ0KDQojIFJlc3VsdDoNCg0KaW1wb3J0IG1hdHBsb3RsaWIucHlwbG90IGFzIHBsdA0KDQpwbHQuc3R5bGUudXNlKCdmaXZldGhpcnR5ZWlnaHQnKQ0KcGx0LmZpZ3VyZShmaWdzaXplPSg4LCA2KSkNCg0KcGx0LnBsb3QoIkN1dG9mZiIsICJBdmdQcm9EZWYiLCBkYXRhPWRmX2F2Z1BybywgbGFiZWw9IkRlZmF1bHQgUkYiLCBsdz0yKQ0KcGx0LnBsb3QoIkN1dG9mZiIsICJBdmdQcm9UdXIiLCBkYXRhPWRmX2F2Z1BybywgbGFiZWw9IlR1cm5lZCBSRiIsIGx3PTIpDQpwbHQudGl0bGUoIkZpZ3VyZSAxOiBQcm9maXQgYmV0d2VlbiBEZWZhdWx0IGFuZCBUdXJuZWQgUkYiLCBmb250c2l6ZT0xMykNCnBsdC54bGFiZWwoIkN1dG9mZiIpDQpwbHQueWxhYmVsKCJQcm9maXQiKQ0KcGx0Lnl0aWNrcyhmb250c2l6ZT0xMikNCnBsdC54dGlja3MoZm9udHNpemU9MTIpDQpwbHQubGVnZW5kKGZvbnRzaXplPTgpDQpwbHQuc2hvdygpDQpgYGANCg0KDQoNCiANCg==