簡體   English   中英

從python中的xgboost中提取決策規則

[英]extracting decision rules from xgboost in python

我想在 python 中為我即將推出的模型使用 xgboost。 然而,由於我們的生產系統是在 SAS 中,我試圖從 xgboost 中提取決策規則,然后編寫一個 SAS 評分代碼來在 SAS 環境中實現這個模型。

我已經通過了多個鏈接到此。 以下是其中一些:

如何從 python3 中的 xgboost 模型中提取決策規則(特征分割)?

xgboost 部署

上面兩個鏈接,特別是 Shiutang-Li 給出的用於 xgboost 部署的代碼,有很大的幫助。 但是,我的預測分數並不完全匹配。

以下是我到目前為止嘗試過的代碼:

import numpy as np
import pandas as pd
import xgboost as xgb
from sklearn.grid_search import GridSearchCV
%matplotlib inline
import graphviz
from graphviz import Digraph

#Read the sample iris data:
iris =pd.read_csv("C:\\Users\\XXXX\\Downloads\\Iris.csv")
#Create dependent variable:
iris.loc[iris["class"] != 2,"class"] = 0
iris.loc[iris["class"] == 2,"class"] = 1

#Select independent and dependent variable:
X = iris[["sepal_length","sepal_width","petal_length","petal_width"]]
Y = iris["class"]

xgdmat = xgb.DMatrix(X, Y) # Create our DMatrix to make XGBoost more efficient

#Build the sample xgboost Model:

our_params = {'eta': 0.1, 'seed':0, 'subsample': 0.8, 'colsample_bytree': 0.8, 
             'objective': 'binary:logistic', 'max_depth':3, 'min_child_weight':1} 
Base_Model = xgb.train(our_params, xgdmat, num_boost_round = 10)

#Below code reads the dump file created by xgboost and writes a scoring code in SAS:

import re
def string_parser(s):
    if len(re.findall(r":leaf=", s)) == 0:
        out  = re.findall(r"[\w.-]+", s)
        tabs = re.findall(r"[\t]+", s)
        if (out[4] == out[8]):
            missing_value_handling = (" or missing(" + out[1] + ")")
        else:
            missing_value_handling = ""

        if len(tabs) > 0:
            return (re.findall(r"[\t]+", s)[0].replace('\t', '    ') + 
                    '        if state = ' + out[0] + ' then do;\n' +
                    re.findall(r"[\t]+", s)[0].replace('\t', '    ') +
                    '            if ' + out[1] + ' < ' + out[2] + missing_value_handling +
                    ' then state = ' + out[4] + ';' +  ' else state = ' + out[6] + ';\nend;' ) 
        else:
            return ('        if state = ' + out[0] + ' then do;\n' +
                    '            if ' + out[1] + ' < ' + out[2] + missing_value_handling +
                    ' then state = ' + out[4] + ';' +  ' else state = ' + out[6] + ';\nend;' )
    else:
        out = re.findall(r"[\w.-]+", s)
        return (re.findall(r"[\t]+", s)[0].replace('\t', '    ') + 
                '        if state = ' + out[0] + ' then\n    ' +
                re.findall(r"[\t]+", s)[0].replace('\t', '    ') + 
                '        value = value + (' + out[2] + ') ;\n')

def tree_parser(tree, i):
    return ('state = 0;\n'
             + "".join([string_parser(tree.split('\n')[i]) for i in range(len(tree.split('\n'))-1)]))

def model_to_sas(model, out_file):
    trees = model.get_dump()
    result = ["value = 0;\n"]
    with open(out_file, 'w') as the_file:
        for i in range(len(trees)):
            result.append(tree_parser(trees[i], i))
        the_file.write("".join(result))
        the_file.write("\nY_Pred1 = 1/(1+exp(-value));\n")
        the_file.write("Y_Pred0 = 1 - Y_pred1;") 

調用上述模塊創建SAS評分代碼:

model_to_sas(Base_Model, 'xgb_scr_code.sas')

不幸的是,我無法提供上述模塊生成的完整 SAS 代碼。 但是,如果我們僅使用一個樹代碼構建模型,請在 SAS 代碼下方找到:

value = 0;
state = 0;
if state = 0 then
    do;
        if sepal_width < 2.95000005 or missing(sepal_width) then state = 1;
        else state = 2;
    end;
if state = 1 then
    do;
        if petal_length < 4.75 or missing(petal_length) then state = 3;
        else state = 4;
    end;

if state = 3 then   value = value + (0.1586207);
if state = 4 then   value = value + (-0.127272725);
if state = 2 then
    do;
        if petal_length < 3 or missing(petal_length) then state = 5;
        else state = 6;
    end;
if state = 5 then   value = value + (-0.180952385);
if state = 6 then
    do;
        if petal_length < 4.75 or missing(petal_length) then state = 7;
        else state = 8;
    end;
if state = 7 then   value = value + (0.142857149);
if state = 8 then   value = value + (-0.161290333);

Y_Pred1 = 1/(1+exp(-value));
Y_Pred0 = 1 - Y_pred1;

下面是第一棵樹的轉儲文件輸出:

booster[0]:
    0:[sepal_width<2.95000005] yes=1,no=2,missing=1
        1:[petal_length<4.75] yes=3,no=4,missing=3
            3:leaf=0.1586207
            4:leaf=-0.127272725
        2:[petal_length<3] yes=5,no=6,missing=5
            5:leaf=-0.180952385
            6:[petal_length<4.75] yes=7,no=8,missing=7
                7:leaf=0.142857149
                8:leaf=-0.161290333

所以基本上,我想做的是,將節點號保存在變量“狀態”中並相應地訪問葉節點(我從上述鏈接中提到的 Shiutang-Li 的文章中了解到)。

這是我面臨的問題:

對於多達大約 40 棵樹,預測分數完全匹配。 例如請看下面:

情況1:

使用 python 預測 10 棵樹的值:

Y_pred1 = Base_Model.predict(xgdmat)

print("Development- Y_Actual: ",np.mean(Y)," Y predicted: ",np.mean(Y_pred1))

輸出:

Average- Y_Actual:  0.3333333333333333  Average Y predicted:  0.4021197

使用 SAS 對 10 棵樹的預測值:

Average Y predicted:  0.4021197

案例二:

使用 python 預測 100 棵樹的值:

Y_pred1 = Base_Model.predict(xgdmat)

print("Development- Y_Actual: ",np.mean(Y)," Y predicted: ",np.mean(Y_pred1))

輸出:

Average- Y_Actual:  0.3333333333333333  Average Y predicted:  0.33232176

使用 SAS 對 100 棵樹的預測值:

Average Y predicted:  0.3323159

如您所見,100 棵樹的分數並不完全匹配(最多匹配 4 個小數點)。 此外,我已經在分數差異很大的大文件上嘗試過這個,即分數偏差超過 10%。

誰能讓我指出我的代碼中的任何錯誤,以便分數可以完全匹配。 以下是我的一些查詢:

1)我的分數計算是否正確。

2)我發現了一些與伽瑪(正則化項)有關的東西。 它是否會影響 xgboost 使用葉值計算分數的方式。

3)轉儲文件給出的葉子值是否會四舍五入,從而產生此問題

此外,除了解析轉儲文件之外,我將不勝感激任何其他方法來完成此任務。

PS:我只有 SAS EG,無法訪問 SAS EM 或 SAS IML。

我在獲得匹配分數方面有類似的經驗。
我的理解是,除非您修復ntree_limit選項以匹配您在模型擬合期間使用的n_estimators ,否則評分可能會提前停止。

df['score']= xgclfpkl.predict(df[xg_features], ntree_limit=500)

在我開始使用ntree_limit ,我開始獲得匹配的分數。

我有類似的經驗,需要將 xgboost 評分代碼從 R 提取到 SAS。

最初,我遇到了與您在這里相同的問題,即在較小的樹中,R 和 SAS 的分數沒有太大差異,一旦樹的數量增加到 100 或更多,我開始觀察差異.

我做了三件事來縮小差異:

  1. 確保丟失的組朝着正確的方向前進,您需要明確。 否則 SAS 會將缺失值視為所有數字中的最小值。 規則應該類似於 SAS 中的以下內容。

if sepal_width > 2.95000005 or missing(sepal_width) then state = 1;else state = 2;
或者
if sepal_width <= 2.95000005 and ~missing(sepal_width) then state = 1;else state = 2;

  1. 我使用了一個名為float的 R 包來使分數有更多的小數位。 as.numeric(float::fl(Quality))

  2. 確保 SAS 數據與您在 Python 中訓練的數據具有相同的形狀。

希望以上有幫助。

我有點想把它合並到我自己的代碼中。

我發現缺少處理存在一個小問題。

在你有這樣的邏輯的地方似乎工作得很好

if petal_length < 3 or missing(petal_length) then state = 5;
        else state = 6;

但是說丟失的組應該轉到狀態 6 而不是狀態 5。然后你會得到這樣的代碼:

if petal_length < 3 then state = 5;
        else state = 6;

petal_length = missing (.)在這種情況下得到什么狀態? 那么在這里它仍然進入狀態 5(而不是預期的狀態 6),因為在 SAS 中丟失被歸類為小於任何數字。

要解決此問題,您可以將所有缺失值分配給 999999999999999(選擇一個較大的數字,因為 XGBoost 格式始終使用小於 (<)),然后替換

missing_value_handling = (" or missing(" + out[1] + ")")

missing_value_handling = (" or " + out[1] + "=999999999999999 ")

在您的string_parser

幾點——

首先,正則表達式葉返回值匹配捕獲垃圾堆里的“E-小數”科學記數法(默認)。 顯式示例(第二個是正確的修改!)-

s = '3:leaf=9.95066429e-09'
out = re.findall(r"[\d.-]+", s)
out2 = re.findall(r"-?[\d.]+(?:e-?\d+)?", s)
out2,out

(易於修復但不易發現,因為我的模型中只有一片葉子受到影響!)

其次,問題是關於二進制的,但在多類目標中,轉儲中的每個類都有單獨的樹,因此總共有T*C樹,其中T是提升輪數, C是類數。 對於c類(在 {0,1,...,C-1} 中),您需要為i = 0,...,T-1評估(並求和)樹i*C +c終葉。 然后將其 softmax 以匹配來自 xgb 的預測。

下面是打印從 xgboost 模型的 booster 樹中提取的所有規則的代碼片段。 以下代碼假定您已經將模型打包到 pickle 文件中。

import pandas as pd
import numpy as np
import pickle
import networkx as nx

_model = pickle.load(open(MODEL_FILE, "rb"))

df = _model._Booster.trees_to_dataframe()
df['_missing'] = df.apply(
    lambda x: 'Yes' if pd.notnull(x['Missing']) and pd.notnull(x['Yes']) and pd.notnull(x['No']) and x['Missing'] == x[
        'Yes'] else 'No', axis = 1)

G = nx.DiGraph()
G.add_nodes_from(df.ID.tolist())

yes_edges = df[['ID', 'Yes', 'Feature', 'Split', '_missing']].dropna()
yes_edges['label'] = yes_edges.apply(
    lambda x: "({feature} < {value:.4f} or {feature} is null)".format(feature = x['Feature'], value = x['Split']) if x['_missing'] == 'Yes'
    else "({feature} < {value:.4f})".format(feature = x['Feature'], value = x['Split']),
    axis = 1
)

no_edges = df[['ID', 'No', 'Feature', 'Split', '_missing']].dropna()
no_edges['label'] = no_edges.apply(
    lambda x: "({feature} >= {value:.4f} or {feature} is null)".format(feature = x['Feature'], value = x['Split']) if x['_missing'] == 'No'
    else "({feature} >= {value:.4f})".format(feature = x['Feature'], value = x['Split']),
    axis = 1
)

for v in yes_edges.values:
    G.add_edge(v[0], v[1], feature = v[2], expr = v[5])

for v in no_edges.values:
    G.add_edge(v[0], v[1], feature = v[2], expr = v[5])

leaf_node_score_values = {i[0]: i[1] for i in df[df.Feature == 'Leaf'][['ID', 'Gain']].values}
nodeID_to_tree_map = {i[1]: i[0] for i in df[['Tree', 'ID']].values}

roots = []
leaves = []
for node in G.nodes:
    if G.in_degree(node) == 0:  # it's a root
        roots.append(node)
    elif G.out_degree(node) == 0:  # it's a leaf
        leaves.append(node)

paths = []
for root in roots:
    for leaf in leaves:
        for path in nx.all_simple_paths(G, root, leaf):
            paths.append(path)

rules = []
temp = []
for path in paths:
    parts = []
    for i in range(len(path) - 1):
        parts.append(G[path[i]][path[i + 1]]['expr'])
    rules.append(" and ".join(parts))
    temp.append((
        path[0],
        nodeID_to_tree_map.get(path[0]),
        " and ".join(parts),
        leaf_node_score_values.get(path[-1])
    ))

rules_df = pd.DataFrame.from_records(temp, columns = ['node', 'tree', 'rule', 'score'])
rules_df['prob'] = rules_df.apply(lambda x: 1 / (1 + np.exp(-1 * x['score'])), axis = 1)
rules_df['rule_idx'] = rules_df.index
rules_df = rules_df.drop(['node'], axis = 1)

print("n_rules -> {}".format(len(rules_df)))

del G, df, roots, leaves, yes_edges, no_edges, temp, rules

上面的代碼以如下格式打印每條規則:

if x>y and a>b and c<d then e

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM