티스토리 뷰

결정트리는 Flow-chart와 같이 조건을 가지고 있는 노드를 생성하고 해당 노드에서 참, 거짓을 판단하여 다음 노드로 향하는, Tree Sort 와 비슷한 모델이다. 

 

데이터의 특성에 맞는 조건을 생성하고, 밑으로 쭉쭉 가지를 늘려가며 양성, 음성 클래스를 판단한 뒤, 노드로 떨어지는 샘플의 갯수로 분류를 진행한다.

 

결정 트리가 가지고 있는 장점은 데이터를 왜 이렇게 예측했는지 설명하기 용이하며, 따로 데이터의 전처리 과정이 필요가 없기 때문이다.

 

아래 예제와 함께 알아보도록 하자.


우선, 결정 트리를 알아보기 전에 직전 포스팅에서 다뤄본 로지스틱 회귀 모델로 데이터를 분류해보려고 한다.

 

데이터는 와인의 종류(레드, 화이트)와 도수, 당도, 산도의 컬럼을 가지고 있다.

 

아래와 같이 데이터를 준비하고, 로지스틱 회귀로 레드와인, 화이트 와인을 분류하는 모델을 만들자.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

# 데이터 준비
wine = pd.read_csv('https://bit.ly/wine_csv_data')

# 맨 위 5개 데이터 확인
print(wine.head())
#  alcohol  sugar    pH  class
# 0      9.4    1.9  3.51    0.0
# 1      9.8    2.6  3.20    0.0
# 2      9.8    2.3  3.26    0.0
# 3      9.8    1.9  3.16    0.0
# 4      9.4    1.9  3.51    0.0

# 데이터프레임의 각 열의 데이터 타입과 누락된 데이터가 있는지 확인
wine.info()
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 6497 entries, 0 to 6496
# Data columns (total 4 columns):
#  #   Column   Non-Null Count  Dtype
# ---  ------   --------------  -----
#  0   alcohol  6497 non-null   float64
#  1   sugar    6497 non-null   float64
#  2   pH       6497 non-null   float64
#  3   class    6497 non-null   float64
# dtypes: float64(4)
# memory usage: 203.2 KB

# 데이터 열에 대한 간단한 통계 값 출력
print(wine.describe())
#            alcohol        sugar           pH        class
# count  6497.000000  6497.000000  6497.000000  6497.000000
# mean     10.491801     5.443235     3.218501     0.753886
# std       1.192712     4.757804     0.160787     0.430779
# min       8.000000     0.600000     2.720000     0.000000
# 25%       9.500000     1.800000     3.110000     1.000000
# 50%      10.300000     3.000000     3.210000     1.000000
# 75%      11.300000     8.100000     3.320000     1.000000
# max      14.900000    65.800000     4.010000     1.000000

# input 과 target 데이터로 나눔
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

# test_size = 0.2 -> 샘플 갯수가 충분히 많으므로, 20% 정도만 테스트 세트로 나눈다.
train_input, test_input, train_target, test_target = train_test_split(data, target, test_size = 0.2, random_state=42)

# 포준화
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

# 로지스틱 회귀를 돌려보자
lr = LogisticRegression()
lr.fit(test_scaled, test_target)

print(lr.score(train_scaled, train_target))
# 0.7869924956705792

print(lr.score(test_scaled, test_target))
# 0.7861538461538462

정확도가 그렇게 높지는 않다. 모호한 데이터가 많아, 분류가 생각만큼 정확하게 되지 않나보다. 매개변수를 바꾸거나 알고리즘을 변경하여 정확도를 높일 순 있겠지만, 결정 트리를 다뤄보려고 하니, 따로 진행하지는 않겠다.

 

그럼 만약에 머신 러닝이나 데이터를 전혀 모르는 어떤 사람에게 이러한 예측의 근거를 설명하려고 하면 어떨까?

 

로지스틱 회귀 모델의 계수와 절편을 출력하여 방정식과 함께 훈련 샘플과 예측 샘플을 함께 보여주나?

 

print(lr.coef_, lr.intercept_)
# [[ 0.60240363  1.63771815 -0.7606157 ]] [1.73083645]

계수와 절편을 위와 같이 얻어낸 뒤에 설명을 하려고 하면, 어디서부터 설명을 해야할 지 감도 안온다. 

 

하지만 결정 트리는 스무고개와도 같아서 질문을 하나씩 던져 정답을 맞춰나가는 과정을 전부 확인할 수 있기 때문에 예측의 근거를 설명하기가 매우 용이하다.


 

DecisionTreeClassifier 클래스를 사용하여 결정 트리 모델을 한번 만들어보자.

 

from sklearn.tree import DecisionTreeClassifier

dt = DecisionTreeClassifier(random_state=42)
dt.fit(train_scaled, train_target)

print(dt.score(train_scaled, train_target))
# 0.996921300750433

print(dt.score(test_scaled, test_target))
# 0.8592307692307692

로지스틱 회귀를 사용했을 때 보다 점수가 훨씬 높게 나왔다. 그런데 훈련 세트에 너무 과대 적합되어 훈련 세트와 테스트 세트의 점수 차이가 꽤나 심하게 나는 것을 볼 수 있다.

 

왜 이런 결과가 나왔을까?

 

pyplot과 sklearn.tree 에서 제공하는 plot_tree 메서드를 사용하여 그 이유를 찾을 수 있다.

 

plot_tree() 함수를 사용하여 결정 트리를 이해하기 쉬운 트리 그림으로 출력하여 확인할 수 있다.

from sklearn.tree import plot_tree
plt.figure(figsize= (10, 7))
plot_tree(dt)
plt.show()

꽤나 엄청난... 트리가 만들어졌다. 아마도 트리의 최대 깊이를 설정해주지 않아서 훈련 세트의 너무 과대하게 적합이 될 때까지 데이터를 학습시킨 거 같다.

 

이번에는 최대 깊이를 1로 제한하고, 노드의 결정 조건을 우리가 눈으로 확인할 수 있게 출력해보자.

plt.figure(figsize=(10, 7))
plot_tree(dt, max_depth=1, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()

이제 좀 읽을 수 있는 무언가가 출력되었다! 노드를 살펴보면, 맨 위 루트 노드는 당도가 -0.239 이하인지 질문을 한다. 이 조건이 참일 경우 왼쪽 노드로, 거짓일 경우 오른쪽 노드로 샘플을 이동시킨다. 루트 노드의 총 샘플 수는 5197개이며, 이 중 음성 클래스(레드 와인)이 1258개, 양성 클래스(화이트 와인)은 3939개 라는 정보를 value 에서 확인할 수 있다.

 

밑에서 오른쪽 노드를 보면, 당도가 0.204 이하인 조건을 가지고 있고 루트 노드가 거짓인 조건을 만족하는 샘플을 2275개 가지고 있으며, 해당 샘플 중 양성 클래스(화이트 와인)의 갯수가 압도적으로 많다.  

 

plot_tree의 filled 매개변수를 True로 설정했기 때문에 value 에 양성클래스의 비율이 높으면 높을 수록 노드의 색깔이 진해진다.

 

결정 트리에서 값을 예측하는 방법은 아주 간단하다. 리프 노드(맨 마지막 노드)에서 value 탭이 가지고 있는 샘플의 갯수가 많은 클래스가 예측 클래스가 되는 방식이다.


 

그렇다면 위 노드들이 가지고 있는 gini라는 값은 뭘까?

 

DecisionTreeClassifier 클래스는 criterion이라는 데이터 분할 기준을 정하는 매개변수를 가지고 있는데, 위 코드와 같이 매개변수를 지정해주지 않으면, 자동으로 gini를 사용하여 데이터를 분할한다.

 

gini는 지니 불순도를 의미하는데, 이는 아래 수식과 같이 value 값이 가지고 있는 데이터의 불순도를 나타내는 값이다.

 

지니 불순도 = 1 - (음성 클래스 비율의 제곱 + 양성 클래스 비율의 제곱)

 

지니 불순도는 0일 때, value = [0, 100] 과 같이 다른 종류의 샘플이 아예 없는 깔끔한 순수 노드가 되며, 0.5일 때 최악의 노드가 된다.

 

졀정 트리 모델은 부모 노드와 자식 노드의 불순도 차이가 가능한 크도록 트리를 성장시킨다. 

부모 노드와 자식 노드의 불순도 차이는 자식 노드의 불순도를 샘플 개수에 비례하여 모두 더한 뒤, 부모 노드의 불순도에서 빼면 된다.

 

불순도 차이 = 부모의 불순도 -

(왼쪽 노드 샘플 수 / 부모의 샘플 수) x 왼쪽 노드 불순도 -

(오른쪽 노드 샘플 수 / 부모의 샘플 수) x 오른쪽 노드 분순도 

 

이런 부모와 자식 노드 사이의 불순도 차이를 정보 이득이라고 부르며, 결정 트리는 정보 이득이 최대가 되도록 데이터를 나눈다.


이제, 위 코드와는 다르게 트리가 자라날 수 있는 최대 깊이를 지정하여 학습시켜보자. 결정 트리에서는 이를 가지치기라고 한다.

 

dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_scaled, train_target)

print(dt.score(train_scaled, train_target))
# 0.8454877814123533

print(dt.score(test_scaled, test_target))
# 0.8415384615384616

plt.figure(figsize=(20, 15))
plot_tree(dt, max_depth=3, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()

훈련 세트의 성능은 조금 떨어졌지만, 테스트 세트와의 점수 차이는 거의 나지 않을 정도로 과대 적합의 문제는 해결이 되었다. 

 

그러나 노드를 확인해보면, 당도가 - 값을 가지고 있는 것을 확인할 수 있다. 

 

이는 위에서 데이터를 로지스틱 회귀 모델을 적용시키기 위해 전처리하는 과정에서 다른 특성과 스케일을 맞추기 위해 임의로 조정된 값을 가지고 있는 것인데, 사실 결정 트리는 조건으로 샘플을 나누어 데이터를 분류하기 때문에 스케일을 굳이 맞춰줄 필요가 없다.

 

dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_input, train_target)

print(dt.score(train_scaled, train_target))
# 0.8454877814123533

print(dt.score(test_scaled, test_target))
# 0.8415384615384616

plt.figure(figsize=(20, 15))
plot_tree(dt, max_depth=3, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()

 이렇게 스케일이 되지 않은 데이터로 분류해도 성능이 거의 동일하게 나온다.

 

이제 트리의 조건이 원본 데이터와 스케일이 동일하여 훨씬 이해하기 쉬운 트리가 되었다. 


마지막으로 결정 트리는 어떤 특성이 가장 유용한지 나타내는 특성 중요도를 계산해준다. 

 

특성 중요도는 트리의 루트 노드가 사용한 당도가 가장 유력해 보이는데, 이는 feature_importances_ 속성에서 확인할 수 있다.

print(dt.feature_importances_)
# [0.12345626 0.86862934 0.0079144 ]

 

역시 2번째 특성인 당도가 약 0.87 정도로 가장 중요도가 높다. 

 

 

 

이렇게 결정 트리를 사용하면, 비교적 비전문가에게도 설명하기 쉬운 모델을 구성할 수 있다.

 

 

 

끝!

Comments