다시 이음

트리 기반 모델 - 1 결정 트리 (Decision Trees) 본문

AI 일별 공부 정리

트리 기반 모델 - 1 결정 트리 (Decision Trees)

Taeho(Damon) 2021. 8. 18. 00:21

오늘은 트리기반(tree-based) 머신러닝모델에 대해서 살펴보려고 합니다.

 

결정트리(의사결정나무) 모델은 특성들을 기준으로 샘플을 분류해 나가는데 그 형태가 나무의 가지가 뻗어나가는 모습과 비슷해서 결정트리라는 이름을 가지고 있습니다. 

 

결정트리의 구성

 

질문이나 말단의 정답을 노드(node) 라 하며 노드를 연결하는 선을 엣지(edge) 라 합니다.

 

결정트리의 각 노드(node)는 뿌리(root)노드, 중간(internal)노드, 말단(external, leaf, terminal) 노드로 나뉠 수 있습니다.

 

 

결정트리의 특징

 

  • 결정트리는 분류와 회귀문제 모두 적용 가능합니다.
  • 결정트리는 데이터를 분할해 가는 알고리즘입니다.
  • 분류 과정은 새로운 데이터가 특정 말단 노드에 속한다는 정보를 확인한 뒤 말단노드의 빈도가 가장 높은 범주로 데이터를 분류합니다.
  • 결정트리는 분류과정을 트리구조로 직관적으로 확인이 가능한 장점이 있습니다.

 

결정트리 학습 알고리즘

 

결정트리를 학습하는 것은 노드를 어떻게 분할하는가에 대한 문제입니다.

 

노드 분할 방법에 따라 다른 모양의 트리구조가 만들어지게 될 것입니다. 결정트리의 비용함수를 정의하고 그것을 최소화 하도록 분할하는 것이 트리모델 학습 알고리즘이 되겠습니다.

 

트리학습에 자주 쓰이는 비용함수 중 지니불순도와 엔트로피에 대해서 알아보겠습니다.

 

 

지니불순도(Gini Impurity or Gini Index)

 

 

지니불순도에서 불순도(impurity) 라는 개념은 여러 범주가 섞여 있는 정도를 이야기 합니다. 

 

예를들어 A, B 두 클래스가 혼합된 데이터가 있을 때 (A, B) 비율이

  • (45%, 55%)인 샘플(두 범주 수가 비슷)은 불순도가 높은 것이며
  • (80%, 20%)인 샘플이 있다면 상대적으로 위의 상태보다 불순도가 낮은 것 입니다.(순수도(purity)는 높음)

 

 

불순도의 공식을 예시를 들어서 편하게 설명해볼게요.

A예시를 먼저 살펴봅시다.

 

위에서 설명한대로 위에있는 노드를 부모노드 혹은 루트,인터노드로 볼 수 있고 그 밑의 노드를 자식노드, 리프노드라고 볼 수 있습니다.

 

 

 

왼쪽의 리프노드의 지니불순도는 1 - (30/40)^2 - (10/40)^2 = 0.375 이렇게 확률의 제곱을 통해서 확인 할 수 있습니다.

 

똑같이 오른쪽의 리프노드의 지니불순도 1 - (10/40)^2 - (30/40)^2 = 0.375를 구할 수 있습니다.

 

각각 gini1(왼쪽노드 불순도) , gini2(오른쪽 노드 불순도)라고 지칭하겠습니다.

 

부모노드의 지니불순도는 1 - (40/80)^2 - (40/80)^2 = 0.5,

 

자식노드의 지니불순도 총합은 (40(왼쪽노드 개수)/80(리프노드 총 개수)*gini1 + (40(오른쪽노드 개수/80(리프노드 총 개수)*gini2 = 0.125 로 풀어쓸 수 있습니다.

 

하여 A예시의 정보획득량(Information Gain) 는 0.5 - 0.125 = 0.375 로 확인됩니다.

 

 

 

B예시를 빠르게 보면 왼쪽 리프노드는 1- (20/60)^2 - (40/60)^2 =0.4444 으로, 오른쪽 리프노드는 1 - (20/20)^2 - (0/20)^2 = 0 으로 불순도를 확인할 수 있습니다.

 

부모노드의 지니불순도는 1 - (60/80)^2 - (20/80)^2 = 1- 0.5625 - 0.0625 = 0.375 ,

 

자식노드의 지니불순도 총합은 (60/80)*gini1 + (20/80)*gini2 = 0.1666로 볼 수 있습니다.

 

하여 B예시의 정보획득량(Information Gain) 는 0.375 - 0.1666 = 0.2084 로 확인됩니다.

 

 

위에서 살펴본 지니불순도를 통해 어떻게 결정트리에서 노드를 분할하는가 에 대해 정리해보겠습니다.

 

  1.  A예시와 B예시의 부모노드의 불순도를 비교하면 0.375, 0.2084 로 B예시가 불순도가 더 낮다고 볼 수 있음으로 B예시를 우선하여 배치하게 됩니다.
  2.  정보획득(Information Gain)은 특정한 특성을 사용해 분할했을 때 엔트로피의 감소량을 뜻합니다. 분할 전 부모노드 불순도 - 분할 후 자식노드 들의 불순도
  3.  우리는 정보획득이 최대, 부모노드의 지니불순도 - 자식노드의 지니불순도의 차이(불순도의 감소)가 최대가 되는 값을 우선으로 합니다. 
  4.  반대로 정보획득량이 음수, 불순도의 감소가 음수로 되는 경우에는 더이상의 노드 분할이 의미없다고 판단하여 노드 분할을 그만둡니다.

 

 

사이킷런 DesicionTreeClassifier 를 사용해 결정트리를 구현

from sklearn.tree import DecisionTreeClassifier

model = DecisionTreeClassifier(random_state=2, criterion='entropy')
model.fit(X_train_scaled, y_train)
print('훈련 정확도: ', model.score(X_train_scaled, y_train))
print('검증 정확도: ', model.score(X_val_scaled, y_val))

## 파이프라인 활용하여 전처리과정과 함께 구현하기
pipe = make_pipeline(
    OneHotEncoder(use_cat_names=True),  
    SimpleImputer(), 
    DecisionTreeClassifier(random_state=2, criterion='entropy')
)

pipe.fit(X_train, y_train)
print('훈련 정확도: ', pipe.score(X_train, y_train))
print('검증 정확도: ', pipe.score(X_val, y_val))

 

만든 결정트리를 시각화

# graphviz 설치방법: conda install -c conda-forge python-graphviz
import graphviz
from sklearn.tree import export_graphviz

model_dt = pipe.named_steps['decisiontreeclassifier']
enc = pipe.named_steps['onehotencoder']
encoded_columns = enc.transform(X_val).columns

dot_data = export_graphviz(model_dt
                          , max_depth=3
                          , feature_names=encoded_columns
                          , class_names=['no', 'yes']
                          , filled=True
                          , proportion=True)


display(graphviz.Source(dot_data))

## 다른 방법
def show_tree(tree, colnames):
    dot = export_graphviz(tree, feature_names=colnames, filled=True, rounded=True)   
    return graphviz.Source(dot)

 

결정트리의 과적합을 고쳐봅시다.

 

복잡한 트리는 과적합 가능성을 높이기 때문에 복잡도를 낮추어 일반화를 유도합니다.

 

DecisionTreeClassifier 메소드에 사용할 하이퍼 파라미터.

  • min_samples_split ( 노드가 분기할 수 있는 최소한의 샘플 개수 ) : 수치가 높을 수록 과적합이 감소
  • min_samples_leaf ( 말단 노드에 들어갈 수 있는 최소한의 샘플 개수 ) : 수치가 높을 수록 과적합이 감소
  • max_depth ( 분할의 깊이 ) : 수치가 낮을 수록 과적합이 감소
model = DecisionTreeClassifier(min_samples_leaf=20,max_depth=8,min_samples_split=200, random_state=2, criterion='entropy')
model.fit(X_train_scaled, y_train)
print('훈련 정확도: ', model.score(X_train_scaled, y_train))
print('검증 정확도: ', model.score(X_val_scaled, y_val))

 

 

특성중요도(feature importance)

 

특성중요도란

 

마치 회귀분석에서 회귀계수와 같이 타겟과 특성의 관계를 확인할 수 있는 지표입니다.

 

회귀계수와 달리 특성중요도는 항상 양수값을 가집니다. 이 값을 통해 특성이 얼마나 일찍 그리고 자주 분기에 사용되는지 결정됩니다.

# 특성중요도 시각화
import matplotlib.pyplot as plt

importances = pd.Series(model.feature_importances_, X_train.columns)
plt.figure(figsize=(10,30))
importances.sort_values().plot.barh();

 

 

특성상호작용(feature interaction)

 

특성상호작용은 특성들끼리 서로 상호작용을 하는 경우를 말합니다.

 

회귀분석에서는 서로 상호작용이 높은 특성들이 있으면 개별 계수를 해석하는데 어려움이 있고 학습이 올바르게 되지 않을 수 있습니다. 하지만 트리모델은 이런 상호작용을 자동으로 걸러내는 특징이 있습니다.

 

예를 들어, 특성 a,b가 있을 때 a,b가 모두 해당된다면 보너스로 10000 값이 추가적으로 입력될 경우.

 

선형회귀분석모델은 성능이 급격하게 줄어드는 반면, 트리모델은 선형회귀모델과 달리 특성상호작용에도 문제없이 가격을 예측하는 것을 확인할 수 있습니다.

 

 

 

비선형 회귀문제에서 결정트리분석

 

max_depth 값이 낮으면 성능이 선형회귀보다 낮아보이지만 높아질수록 적합되어 학습이 가능합니다.

 

from ipywidgets import interact
from sklearn.tree import DecisionTreeRegressor, export_graphviz

def thurber_tree(max_depth=1):
    tree = DecisionTreeRegressor(max_depth=max_depth)
    tree.fit(X_thurber, y_thurber)
    print('R2: ', tree.score(X_thurber, y_thurber))
    ax = thurber.plot('mobility', 'density', kind='scatter', title='Thuber')
    ax.step(X_thurber, tree.predict(X_thurber), where='mid')
    plt.show()
    display(show_tree(tree, colnames=['mobility']))

interact(thurber_tree, max_depth=(1,6,1));

위의 코드 시각화 결과 / depth값에 따른 r2값의 변화를 확인하세요.