본문 바로가기
프로그래밍/기타 Python 모듈

[XGBoost] XGBoost의 개별 트리로부터 여러 정보(변수 출현 빈도, 예측) 계산하기(feat. get_boost)

by 부자 꽁냥이 2023. 3. 24.

XGBoost 모듈을 이용하면 XGBoost 모형 학습과 예측을 쉽게 해 줄 수 있다. XGBoost 모형을 학습하면 여러개의 트리로 구성되어 있는데 개별 트리로부터 정보를 얻고 싶을 수 있다. 예를 들면 (1) 특정 개별 트리에서 분리 변수의 출현 빈도 또는 (2) 개별 트리의 예측값을 알고 싶을 때가 있다. 이때 get_booster라는 녀석을 사용하면 이러한 정보들을 얻을 수 있다. 이번 포스팅에서는 get_booster를 이용하여 얻은 개별 트리의 분리 변수 출현 빈도, 예측값을 계산하는 방법을 알아보고자 한다.

 

XGBoost의 개념과 XGBoost 모듈에 대한 기본적인 사용법이 궁금하신 분들은 아래 포스팅을 참고하면 된다.

 

[XGBoost] XGBoost 모형 학습하기 (feat. XGBClassifier, XGBRegressor)

 

[XGBoost] XGBoost 모형 학습하기 (feat. XGBClassifier, XGBRegressor)

XGBoost 모듈에는 XGBoost 모형을 학습할 수 있는 다양하고 강력한 기능을 제공한다. 이번 포스팅에서는 XGBoost를 이용한 XGBoost 모형을 학습하고 결과를 확인하는 방법을 알아보려고 한다. XGBoost는 분

zephyrus1111.tistory.com

 

 

21. XGBoost에 대해서 알아보자

 

21. XGBoost에 대해서 알아보자

이번 포스팅에서는 부스팅 계열에 떠오르는 샛별 XGBoost에 대해서 알아보려고 한다. 여기에서는 XGBoost의 개념, 알고리즘 동작 원리를 예제와 함께 알아보고자 한다. - 목차 - 1. XGBoost란 무엇인가?

zephyrus1111.tistory.com


   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) ## 두 어레이는 같은가?

 


댓글


맨 위로