XGBoost 모듈을 이용하면 XGBoost 모형 학습과 예측을 쉽게 해 줄 수 있다. XGBoost 모형을 학습하면 여러개의 트리로 구성되어 있는데 개별 트리로부터 정보를 얻고 싶을 수 있다. 예를 들면 (1) 특정 개별 트리에서 분리 변수의 출현 빈도 또는 (2) 개별 트리의 예측값을 알고 싶을 때가 있다. 이때 get_booster라는 녀석을 사용하면 이러한 정보들을 얻을 수 있다. 이번 포스팅에서는 get_booster를 이용하여 얻은 개별 트리의 분리 변수 출현 빈도, 예측값을 계산하는 방법을 알아보고자 한다.
XGBoost의 개념과 XGBoost 모듈에 대한 기본적인 사용법이 궁금하신 분들은 아래 포스팅을 참고하면 된다.
[XGBoost] XGBoost 모형 학습하기 (feat. XGBClassifier, XGBRegressor)
get_booster 사용법
여기서는 회귀 문제와 분류 문제로 나누어서 살펴본다. 왜냐하면 각 문제별로 트리 구성이 다르기 때문이다.
1) 회귀 문제
먼저 Boston 집값 데이터를 이용하여 XGBoost 회귀 모형을 학습한다.
import xgboost as xgb
from sklearn.datasets import load_boston
from xgboost.sklearn import XGBRegressor
boston = load_boston()
X, y = boston.data, boston.target
## XGBoost 학습
reg = XGBRegressor(
n_estimators=50, ## 붓스트랩 샘플 개수 또는 base_estimator 개수
max_depth=5, ## 개별 나무의 최대 깊이
gamma = 0, ## gamma
importance_type='gain', ## gain, weight, cover, total_gain, total_cover
reg_lambda = 1, ## tuning parameter of l2 penalty
random_state=100
).fit(X,y)
a. get_booster
회귀 모형을 학습했으면 get_booster를 통해 개별 트리 정보를 가져올 수 있다. get_booster를 호출하면 Booster 객체가 생성되는데 feature_names 속성을 통해 변수명을 지정할 수 있다. Booster 객체는 제너레이터 타입이라 list에 감싸주면 개별 트리에 접근할 수 있으며 이 또한 Booster 객체이다.
reg_booster = reg.get_booster() ## Booster 객체 생성
reg_booster.feature_names = list(boston.feature_names) ## 변수명 지정
print('객체 타입 :', type(reg_booster))
individual_trees = list(reg_booster)
print('개별 트리 개수 :', len(individual_trees))
print('개별 트리 객체 타입 :', type(individual_trees[0]))
개별 트리 개수는 모형 학습시 설정했던 n_estimators와 같다.
b. 개별 트리 정보 얻기
이번엔 개별 트리로부터 여러 정보를 얻는 방법을 알아보자.
(1) 변수 출현 빈도
get_score를 이용하면 각 개별 트리에서 분리 시 변수 출현 빈도를 계산할 수 있다. 이때 importance_type='weight'로 해주어야 한다(이는 디폴트 값이다).
tree = individual_trees[0] ## 첫 번째 트리
tree.get_score(importance_type='weight') ## 디폴트
즉, 첫 번째 개별 트리에서는 분리 시 CRIM은 2번, NOX는 1번, RM은 3번 출현했음을 알 수 있다.
(2) 변수 중요도
여기서 말하는 변수 중요도는 일종의 MDI 기반 변수 중요도이며 이 또한 get_score에서 importance_type='gain'으로 설정하면 된다.
tree = individual_trees[0] ## 첫 번째 트리
tree.get_score(importance_type='gain') ## 디폴트
(3) 개별 트리로 예측하기
predict를 이용하면 개별 트리로부터 예측을 수행할 수 있다. 이때 데이터를 DMatrix로 변환해야하며 변수명이 있다면 feature_names 인자에 변수명이 담긴 배열을 전달해야 한다.
tree = individual_trees[0] ## 첫 번째 트리
test_data = X[0].reshape(1, -1)
## 예측을 위해 예측 데이터를 Numpy array에서 DMatrix로 변환
test_data = xgb.DMatrix(test_data, feature_names=reg_booster.feature_names)
tree.predict(test_data) ## 예측
(4) 사람이 이해할 수 있는 트리로 변환하기
get_dump를 이용하면 사람이 이해할 수 있는 텍스트, json, 그래프 형태로 볼 수 있다. get_dump를 출력하면 주어진 포맷을 가진 개별 트리를 원소로 하는 리스트가 생성된다. 여기서는 json 형태를 딕셔너리 형태로 바꾸어보자.
import json
## with_stats True이면 gain과 같은 통계량 출력
dump = reg_booster.get_dump(dump_format='json', with_stats=True)
tree_dict = json.loads(dump[0]) ## 첫 번째 개별 트리
tree_dict
{'nodeid': 0,
'depth': 0,
'split': 'LSTAT',
'split_condition': 9.72500038,
'yes': 1,
'no': 2,
'missing': 1,
'gain': 18247.6094,
'cover': 506,
'children': [{'nodeid': 1,
'depth': 1,
'split': 'RM',
'split_condition': 6.94099998,
'yes': 3,
'no': 4,
'missing': 3,
'gain': 6860.23438,
'cover': 212,
'children': [{'nodeid': 3,
'depth': 2,
'split': 'DIS',
'split_condition': 1.48494995,
'yes': 7,
'no': 8,
'missing': 7,
'gain': 564.898438,
'cover': 142,
'children': [{'nodeid': 7, 'leaf': 11.8800001, 'cover': 4},
{'nodeid': 8,
'depth': 3,
'split': 'RM',
'split_condition': 6.54300022,
'yes': 15,
'no': 16,
'missing': 15,
'gain': 113.882813,
'cover': 138,
'children': [{'nodeid': 15, 'leaf': 6.64261389, 'cover': 87},
{'nodeid': 16, 'leaf': 8.01750088, 'cover': 51}]}]},
{'nodeid': 4,
'depth': 2,
'split': 'RM',
'split_condition': 7.43700027,
'yes': 9,
'no': 10,
'missing': 9,
'gain': 713.554688,
'cover': 70,
'children': [{'nodeid': 9, 'leaf': 9.67683029, 'cover': 40},
{'nodeid': 10,
'depth': 3,
'split': 'CRIM',
'split_condition': 2.74223518,
'yes': 17,
'no': 18,
'missing': 17,
'gain': 260.210938,
'cover': 30,
'children': [{'nodeid': 17, 'leaf': 13.165, 'cover': 29},
{'nodeid': 18, 'leaf': 3.21000004, 'cover': 1}]}]}]},
{'nodeid': 2,
'depth': 1,
'split': 'LSTAT',
'split_condition': 16.0849991,
'yes': 5,
'no': 6,
'missing': 5,
'gain': 2385.59375,
'cover': 294,
'children': [{'nodeid': 5,
'depth': 2,
'split': 'B',
'split_condition': 116.024994,
'yes': 11,
'no': 12,
'missing': 11,
'gain': 118.414063,
'cover': 150,
'children': [{'nodeid': 11, 'leaf': 3.54750013, 'cover': 7},
{'nodeid': 12, 'leaf': 5.99104166, 'cover': 143}]},
{'nodeid': 6,
'depth': 2,
'split': 'NOX',
'split_condition': 0.603000045,
'yes': 13,
'no': 14,
'missing': 13,
'gain': 639.9375,
'cover': 144,
'children': [{'nodeid': 13, 'leaf': 5.06040001, 'cover': 49},
{'nodeid': 14,
'depth': 3,
'split': 'CRIM',
'split_condition': 11.3691502,
'yes': 19,
'no': 20,
'missing': 19,
'gain': 346.749023,
'cover': 95,
'children': [{'nodeid': 19,
'depth': 4,
'split': 'AGE',
'split_condition': 77.9499969,
'yes': 21,
'no': 22,
'missing': 21,
'gain': 40.9677734,
'cover': 63,
'children': [{'nodeid': 21, 'leaf': 0.870000064, 'cover': 1},
{'nodeid': 22, 'leaf': 4.03857136, 'cover': 62}]},
{'nodeid': 20, 'leaf': 2.5854547, 'cover': 32}]}]}]}]}
코드를 실행하면 위와 같이 사람이 (그나마) 이해하기 쉬운 형태로 출력되는 것을 알 수 있다. 위 결과에서 각 속성의 의미는 다음과 같다.
딕셔너리로 구성한 개별 트리를 이용해서 변수 출현 빈도와 예측값을 계산할 수 있다. 아래 함수는 딕셔너리 형태의 트리에 대한 변수 출현 빈도를 계산한다.
rom collections import Counter
def get_features_ocurrence(dict_data, res):
if 'children' not in dict_data and dict_data['nodeid'] == 0:
## Root 노드만 있는 경우 스플릿 변수를 추가한 뒤 리턴
res.append(dict_data['split'])
return Counter(res)
elif 'leaf' in dict_data:
return
else:
res.append(dict_data['split'])
for dict_elem in dict_data['children']:
get_features_ocurrence(dict_elem, res)
return Counter(res)
위 함수를 이용해서 첫 번째 트리의 변수 출현 빈도를 살펴보자.
## 개별 트리에서의 변수 발생 빈도
get_features_ocurrence(tree_dict, [])
코드를 실행하면 앞에서 get_score를 이용한 방법과 동일한 결과를 얻을 수 있다. 이번엔 딕셔너리 형태의 트리를 이용하여 예측을 수행해 보자. 아래 함수는 예측할 데이터, 딕셔너리 형태의 트리 그리고 변수명을 입력받아서 예측값을 계산한다.
def predict_from_individual_tree(data, dict_data, feature_names):
if 'leaf' in dict_data:
return dict_data['leaf']
else:
split_feature = dict_data['split'] ## 분리 변수
feature_idx = feature_names.index(split_feature) ## 변수 인덱스
split_value = dict_data['split_condition'] ## 분리 기준
flag = 'missing'
if data[feature_idx] < split_value:
flag = 'yes'
else:
flag = 'no'
child_node_id = dict_data[flag]
children_node = [x for x in dict_data['children'] if x['nodeid'] == child_node_id][0]
return predict_from_individual_tree(data, children_node, feature_names)
data = X[0]
predict_from_individual_tree(data, tree_dict, reg_booster.feature_names)
위 코드를 실행하면 get_booster를 통한 예측값과 동일한 것을 알 수 있다. 참고로 개별 트리를 이용하여 XGBoost 모형의 예측값을 얻으려면 개별 트리별로 예측값을 모두 더한 다음 모형 학습 시 설정한 base_score까지 더해주어야 한다. 실제로 개별 트리로부터 계산된 예측값과 XGBoost 모형으로 계산한 예측값이 동일한 것을 알 수 있다(소수점에서 약간 차이는 있지만 ㅎㅎ).
from collections import Counter
import numpy as np
data = X[0]
feature_names = reg_booster.feature_names ## booster 객체의 변수명 속성
predict_res = []
count_dict = Counter()
for d in dump:
dict_data = json.loads(d)
predict_val = predict_from_individual_tree(data, dict_data, feature_names)
predict_res.append(predict_val)
base_score = reg.get_params()['base_score']
print(np.sum(predict_res)+base_score) ## 개별 트리로부터 얻은 예측값
print(reg.predict(data.reshape(1, -1))) ## XGBoost 예측값
data_dm = xgb.DMatrix(data.reshape(1, -1), feature_names=feature_names)
print(reg_booster.predict(data_dm)) ## Booster를 이용한 XGBoost 예측값
2) 분류 문제
이번엔 분류 문제인 경우에 대해서 XGBoost 개별 트리에 대한 정보를 꺼내보자. 먼저 붓꽃 데이터를 이용하여 XGBoost 모형을 학습한다.
from sklearn.datasets import load_iris
from xgboost.sklearn import XGBClassifier
iris = load_iris()
X, y = iris.data, iris.target
clf = XGBClassifier(
n_estimators=50, ## 붓스트랩 샘플 개수 또는 base_estimator 개수
max_depth=5, ## 개별 나무의 최대 깊이
gamma = 0, ## gamma
importance_type='gain', ## gain, weight, cover, total_gain, total_cover
reg_lambda = 1, ## tuning parameter of l2 penalty
random_state=100,
).fit(X,y)
a. get_booster
get_booster 사용법은 회귀 문제에서와 동일하다.
clf_booster = clf.get_booster() ## Booster 객체 생성
clf_booster.feature_names = list(iris.feature_names) ## 변수명 지정
print('객체 타입 :', type(clf_booster))
individual_trees = list(clf_booster)
print('개별 트리 개수 :', len(individual_trees))
print('개별 트리 객체 타입 :', type(individual_trees[0]))
b. 개별 트리 정보 얻기
개별 트리로부터 여러 정보를 얻는 방법을 알아보자.
(1) 변수 출현 빈도
get_score 메서드에서 importance_type='weight'로 설정하면 개별 트리에서 변수 출현 빈도를 계산할 수 있다.
tree = individual_trees[0]
tree.get_score(importance_type='weight')
(2) 변수 중요도
변수 중요도를 얻기 위해서는 get_score에서 importance_type='gain'으로 지정하면 된다.
tree = individual_trees[0]
tree.get_score(importance_type='gain')
(3) 개별 트리로 예측하기
predict를 이용하면 개별 트리로부터 각 클래스별 확률 예측값을 얻을 수 있다. 붓꽃 데이터는 3개의 클래스가 있으므로 3개의 확률값이 나온다.
tree = individual_trees[0] ## 첫 번째 트리
test_data = X[0].reshape(1, -1)
## 예측을 위해 예측 데이터를 Numpy array에서 DMatrix로 변환
test_data_dm = xgb.DMatrix(test_data, feature_names=clf_booster.feature_names)
tree.predict(test_data_dm) ## 예측
예측 클래스는 확률 예측값들 중에서 가장 큰 값에 해당하는 클래스로 선정한다.
(4) 사람이 이해할 수 있는 트리로 변환하기
get_dump를 이용하면 사람이 이해할 수 있는 포맷으로 트리를 불러올 수 있다.
dump = clf_booster.get_dump(dump_format='json')
print(dump[0]) ## 첫 번째 개별 트리
회귀문제에서와 다르게 get_dump로 반환된 총 개별 트리 개수는 150개이다.
print('개별 트리 개수 :', len(dump))
이는 XGBoost 모형 학습 시 설정한 n_estimators=50과 클래스 개수 3을 곱한 값이다. 왜 이렇게 생성되었냐 하면 개별 트리가 one vs rest 형태이기 때문이다. 이때 get_booster에서의 첫 번째 개별 트리는 get_dump에서의 처음 3개(클래스 개수)의 트리와 같다고 보면 된다.
아래 코드와 같이 처음 3개에 대한 개별 트리에 대하여 변수 출현 빈도를 계산해 보면 앞에서 계산한 것과 동일함을 알 수 있다.
num_class = len(np.unique(y))
count_dict = Counter()
for d in dump[:num_class]:
dict_data = json.loads(d)
count_dict += get_features_ocurrence(dict_data, [])
count_dict
아래 코드는 개별 트리로부터 XGBoost 분류 모형의 예측 클래스를 얻는 과정이다. 이때 0으로 라벨링 된 클래스를 학습하는 개별 트리는 dump[0], dump[3], ... 이고 1로 라벨링된 클래스를 학습하는 개별 트리는 dump[1], dump[2], ... 이런 식이다.
from scipy.special import expit as sigmoid
clf_booster = clf.get_booster()
clf_booster.feature_names = iris.feature_names ## 붓꽃 데이터 변수명 설정
dump = clf_booster.get_dump(dump_format='json')
feature_names = clf_booster.feature_names
labels = np.unique(y)
predict_labels = []
for data in X:
predict_results = []
for l in labels:
predict_values = []
for d in dump[l::len(labels)]:
dict_data = json.loads(d)
predict_value = predict_from_individual_tree(data, dict_data, feature_names)
predict_values.append(predict_value)
predict_results.append(predict_values)
predict_results = np.c_[predict_results].T
summary_results = sigmoid(np.sum(predict_results, axis=0)+0.5) ## 로그 오즈에 시그모이드 함수 적용
predict_labels.append(np.argmax(summary_results)) ## 라벨 예측
predict_labels = np.array(predict_labels)
이는 실제 XGBoost로 학습한 예측 결과와 동일하다.
np.all(clf.predict(X) == predict_labels) ## 두 어레이는 같은가?
'프로그래밍 > 기타 Python 모듈' 카테고리의 다른 글
shapely 모듈에 대해서 알아보자 - 기본편 (0) | 2023.03.29 |
---|---|
dateutil 모듈을 이용하여 datetime 객체 다루기 (0) | 2023.03.25 |
Pyinstaller 기본 - 이용하여 파이썬(.py) 파일을 실행 파일(.exe)로 만들기 (1) | 2023.03.17 |
[오류 해결] Pyinstaller : ValueError: not enough values to unpack (expected 2, got 1) (1) | 2023.03.17 |
[shap] SHAP Value 계산 및 시각화 결과 해석하기 with Python (2) | 2023.01.23 |
댓글