의사결정 나무(Decision Tree)를 학습한 뒤 주어진 데이터가 진행한 노드들의 정보를 알고 싶을 때가 있다. 예를 들어 노드에서 분리할 때 사용된 변수와 분리 기준을 알고 싶을 때가 있을 것이다. 만약 아래 그림에서 의사결정 나무 모형이 있고 주어진 데이터에 대해서 예측을 수행하면 빨간 박스에 있는 노드(마디)들을 진행할 것이다. 이때 진행한 노드 중 중간 노드에 포함된 변수와 분리 기준은 (키, 170), (나이, 30)이 되며 마지막 터미널 노드에서의 예측값은 2인 것을 알 수 있다.
Scikit-Learn(sklearn)에서는 이러한 나무 기반 모형에서 decision_path를 이용하여 주어진 데이터가 진행한 노드 아이디를 알 수 있고 이를 이용하면 진행 노드의 정보를 가져올 수 있는 것이다.
이번 포스팅에서는 decision_path를 이용한 노드 정보 가져오는 방법을 소개한다.

decision_path를 이용한 노드 정보 가져오기
먼저 필요한 모듈을 임포트하고 데이터를 불러온 뒤 의사결정나무를 하나 학습해준다. 데이터는 보스턴 집값 데이터를 사용했다.
import numpy as np import pandas as pd import graphviz import warnings warnings.filterwarnings('ignore') from sklearn.datasets import load_boston, load_iris from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor, export_graphviz boston = load_boston() df = pd.DataFrame(boston.data, columns=boston.feature_names) df['MEDV'] = boston.target X = df.drop('MEDV', axis=1) y = df['MEDV'] reg = DecisionTreeRegressor(max_depth=3).fit(X, y)
의사결정 나무를 시각화해보면 다음과 같다.
dot_data = export_graphviz(reg, out_file=None, feature_names=X.columns, filled=False, rounded=True, special_characters=False) graph = graphviz.Source(dot_data) graph

이제 데이터 하나가 주어졌고 이 데이터의 예측값을 구한다고 해보자.
X.iloc[0]

그렇다면 이 데이터가 진행한 노드들은 아래 그림과 같다.

실제로 예측값을 구해보면 맨 마지막 빨간 박스의 값인 22.905가 나오게 된다.
reg.predict(X.iloc[0].values.reshape(1, -1))

이제 decision_path가 리턴하는 값이 무엇인지 알아야 한다. decision_path는 2차원 배열을 받고 행렬을 리턴한다. 이 행렬의 행은 주어진 2차원 배열의 행을 의미하고 열은 노드 아이디가 된다. 이때 행렬을 M이라 할 때 i번째 행, j 번째 열의 값 mi,j는 0 아니면 1을 갖는다. 그 의미는 i번째 데이터가 노드 아이디 j를 진행했으면 1 아니면 0이라는 뜻이다.

실제로 decisiont_path를 집어넣으면 다음과 같다.
sample_data = X.iloc[0].values.reshape(1, -1) mat = reg.decision_path(sample_data) mat.toarray()

해당 데이터는 0, 1, 2, 4번 노드를 진행했음을 알 수 있다.
이제 주어진 데이터가 진행한 노드의 정보를 가져오는 함수가 아래 코드이다. get_path_node_info 함수는 학습된 모형(model), 확인하고자 할 데이터(sample_data), 변수 이름이 담긴 배열(feature_names) 그리고 모형의 타입(model_type)을 인자로 받는다. 그리고 반환하는 것은 sample_data가 진행한 노드 정보를 가져온다. 노드 정보는 노드가 터미널 노드인지 중간 노드인지에 따라 다르다. 코드에 대한 설명은 주석에 자세하게 써놨으니 참고하기 바란다.
def get_path_node_info(model, sample_data, feature_names = None, model_type='regression'): assert model_type in ['regression', 'classification'] leave_id = model.apply(sample_data) ## 주어진 샘플이 도달한 터미널 노드 아이디 features = model.tree_.feature ## 중간 노드에서 사용된 변수들 threshold = model.tree_.threshold ## 중간 노드의 분리 기준 if model_type == 'regression': predicted_value = model.tree_.value.flatten() ## 해당 노드에서의 예측값 else: predicted_value = np.argmax(model.tree_.value, axis=2).flatten() node_indicator = model.decision_path(sample_data) ## node_id_matrix 출력 results = [] num_row = sample_data.shape[0] for row_id in range(num_row): node_indices = node_indicator.indices[ ## 해당 샘플이 지나간 node_id 출력 node_indicator.indptr[row_id]:node_indicator.indptr[row_id+1] ] temp_list = [] for node_id in node_indices: temp_dict = dict() if leave_id[row_id] == node_id: ## 터미널 노드인가? temp_dict['Node ID'] = node_id ## 노드 아이디 temp_dict['Leaf'] = True ## 터미널 노드 여부 temp_dict['Predicted Value'] = predicted_value[node_id] ## 해당 노드에서의 예측값 else: if feature_names is not None: feature = feature_names[features[node_id]] else: feature = features[node_id] temp_dict['Node ID'] = node_id ## 노드 아이디 temp_dict['Leaf'] = False ## 터미널 노드 여부 temp_dict['Feature'] = feature ## 해당 노드에서의 분리 변수 temp_dict['Threshold'] = threshold[node_id] ## 해당 노드에서의 분리 기준 temp_dict['Condition'] = f'{feature} <= {threshold[node_id]:.3f}' ## 조건 temp_dict['Sample Value'] = sample_data[row_id, features[node_id]] ## 주어진 노드에서의 변수에 대한 데이터 값 if sample_data[row_id, features[node_id]] <= threshold[node_id]: satisfied = 1 else: satisfied = 0 temp_dict['Satisfied'] = satisfied temp_dict['Predicted Value'] = predicted_value[node_id] ## 해당 노드에서의 예측값 출력 temp_list.append(temp_dict) results.append(temp_list) return results
이제 노드 정보를 출력해보자. 중간 노드에는 노드 아이디, 터미널 노드 여부, 해당 변수에 대한 데이터 값 등을 출력한다.
sample_data = X.iloc[0].values.reshape(1, -1) ## 예측하고 싶은 데이터 results = get_path_node_info(reg, sample_data, feature_names = boston.feature_names) for i, res in enumerate(results): print(f'Sample Row : {i+1}') for res_dict in res: node_id = res_dict['Node ID'] leaf = res_dict['Leaf'] pred_val = res_dict['Predicted Value'] if res_dict['Leaf']: print(f'Node ID : {node_id}, Leaf : {leaf}, Predicted : {pred_val:.3f}') else: feature = res_dict['Feature'] threshold = res_dict['Threshold'] sample_value = res_dict['Sample Value'] print(f'Node ID : {node_id}, Leaf : {leaf}, Sample Value : {sample_value:.3f}' +f' Feature(Threshold) : {feature}({threshold:.3f}),' +f' Predicted : {pred_val:.3f}')

이번엔 분류 문제에서도 잘 작동하는지 살펴보자. 데이터는 붓꽃 데이터를 사용했다.
iris = load_iris() df = pd.DataFrame(iris.data, columns=iris.feature_names) df['species'] = iris.target X = df.drop('species', axis=1) y = df['species'] clf = DecisionTreeClassifier(max_depth=3).fit(X, y)
시각화를 해보면 다음과 같다.
dot_data = export_graphviz(clf, out_file=None, feature_names=X.columns, filled=False, rounded=True, special_characters=False) graph = graphviz.Source(dot_data) graph

앞에서는 데이터 하나에 대한 노드 정보를 가져왔다면 이번엔 3개 데이터에 대해서 각 데이터가 진행한 노드 정보를 가져오자.
sample_data = X.iloc[[40, 70, 100]].values ## 예측하고 싶은 데이터 results = get_path_node_info(clf, sample_data, feature_names = iris.feature_names, model_type='classification' ) for i, res in enumerate(results): print(f'Sample Row : {i+1}') for res_dict in res: node_id = res_dict['Node ID'] leaf = res_dict['Leaf'] pred_val = res_dict['Predicted Value'] if res_dict['Leaf']: print(f'Node ID : {node_id}, Leaf : {leaf}, Predicted : {pred_val}') else: feature = res_dict['Feature'] threshold = res_dict['Threshold'] sample_value = res_dict['Sample Value'] print(f'Node ID : {node_id}, Leaf : {leaf}, Sample Value : {sample_value:.3f}' +f' Feature(Threshold) : {feature}({threshold:.3f}),' +f' Predicted : {pred_val}')

댓글