혼자 공부하는 머신러닝 + 딥러닝

[혼공머신] 9. Decision Tree

Seon_ 2021. 12. 18. 18:54
9.wine_classification_DT

Decision Tree

아래 데이터는 캐글의 Red Wine Quality 데이터셋의 일부를 발췌한 것이다.

In [1]:
import pandas as pd

wine = pd.read_csv('https://bit.ly/wine_csv_data')
In [2]:
wine.head()
Out[2]:
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

class = 0 이면 레드와인, 1이면 화이트와인(양성 클래스)

  • 판다스 데이터프레임의 유용한 메서드 2가지
    • info() : 데이터프레임의 각 열의 데이터 타입과 누락된 데이터가 있는지 확인하는데 유용함
In [3]:
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
  • 총 6497개의 entries가 있는데 Non-Null count가 6497이므로 누락된 값은 없다.
  • 다음으로는 describe() 메서드이다.
    • 이 메서드는 열에 대한 간략한 통계를 출력해준다.
    • 최소, 최대, 평균값 등을 볼 수 있다.
In [4]:
wine.describe()
Out[4]:
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
  • 알코올 도수, 당도, pH 값의 스케일이 다르므로 표준화할 필요가 있다.
In [5]:
#input과 target 분리
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy() 
In [6]:
from sklearn.model_selection import train_test_split

train_input, test_input, train_target, test_target = train_test_split(
    data, target, test_size=0.2, random_state=42)
In [7]:
print(train_input.shape, test_input.shape)
(5197, 3) (1300, 3)

훈련 세트 : 5197 개 / 테스트 세트 : 1300 개

In [8]:
from sklearn.preprocessing import StandardScaler

ss = StandardScaler()
ss.fit(train_input)

train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
In [9]:
from sklearn.linear_model import LogisticRegression

lr = LogisticRegression()
lr.fit(train_scaled, train_target)

print(lr.score(train_scaled, train_target))
print(lr.score(test_scaled, test_target))
0.7808350971714451
0.7776923076923077
  • 훈련 세트와 테스트 세트의 스코어가 모두 낮은 것을 보아 과소적합됬음을 알 수 있다.
    • Regulazation factor인 C값을 바꾸어볼 수 있다.
    • solver 매개변수에서 다른 알고리즘을 선택할 수 있다.
    • 다항 특성을 만들어 추가할 수 있다.
In [10]:
print(lr.coef_, lr.intercept_)
[[ 0.51270274  1.6733911  -0.68767781]] [1.81777902]
  • 로지스틱 회귀 모델을 사용하여 완벽한 결과를 만들어낼 수 있지만, 그 결과값을 일반적인 대중에게 설명하기는 어렵다.
    • 특히 다항 특성을 추가한 경우에는 어떤 조치를 해야할지 덜 명확할 수 있다.

Decision Tree

  • 결정 트리는 스무고개와 같이 특정한 기준에 따라 데이터들을 분류한다.
  • 사이킷런에서는 DecisionTreeClassifier 클래스를 사용할 수 있다.
In [11]:
from sklearn.tree import DecisionTreeClassifier

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

print(dt.score(train_scaled, train_target))
print(dt.score(test_scaled, test_target))
0.996921300750433
0.8592307692307692
  • 훈련 세트에 대해서는 정확도가 아주 높지만, 테스트 세트에 대해서는 낮은 것으로 보아 overfitting 되었음을 알 수 있다.
In [12]:
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

plt.figure(figsize=(10,7))
plot_tree(dt)
plt.show()

(이러니 overfitting이 안될 수가 있나 ...)

  • 맨 위의 node 를 root node라고 하고 맨 밑의 node를 leaf node라고 한다.
    • 이 때 node란 훈련 데이터의 특성에 대한 테스트를 표현하며, branch는 테스트의 결과를 나타낸다.
    • 따라서 일반적으로 node는 2개의 branch를 가진다.
In [13]:
plt.figure(figsize=(10,7))
plot_tree(dt, max_depth=1, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()
  • max_depth 매개변수는 root node를 제외하고 깊이를 얼마나 확장할 것인지를 정하는 변수이다.
  • filled 매개변수는 클래스에 맞게 node의 색을 칠할 수 있다.
    • 클래스마다 색깔을 지정하고 특정 클래스의 비율이 높으면 색이 진하게 바뀐다.
  • feature_names 매개변수는 특성의 이름을 전달할 수 있다.
  • 노드에 적혀있는 수치는 위에서부터 차례로 테스트 조건, 불순도, 총 샘플 수, 클래스별 샘플 수를 뜻한다.
    • branch에서 왼쪽은 yes, 오른쪽은 no이다.
    • 클래스별 샘플 수는 각 node에서 예측한 레드와인(1258)과 화이트와인(3939, 양성 클래스)의 수라고 할 수 있다.
  • 여기서 불순도에 gini라고 적혀있는 것은 지니 불순도를 의미한다.
    • DecisionTreeClassifer 클래스에서 gini는 criterion 매개변수의 기본값이다.
    • 지니 불순도 = 1 - (양성 클래스 비율^2 + 음성 클래스 비율^2)이다.
    • 지니 불순도의 경우 양쪽 클래스가 모두 동일하게 존재하는 경우 0.5로 가장 작고, 한쪽에 모두 몰려있는 경우 1.0으로 가장 크다.
    • 결정 트리 모델은 부모 노드와 자식 노드의 불순도 차이가 가능한 크도록 트리를 성장시킨다.
    • 부모 노드와 자식 노드의 불순도 차이는 아래와 같이 계산한다:

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

  • 위와 같이 계산한 부모 노드와 자식 노드 사이의 불순도 차이를 정보 이득(information gain)이라고 한다.
  • 한편 criterion 매개변수에 gini가 아니라 entropy를 사용할 수 있는데, 이 경우는 아래와 같이 계산한다:

-음성 클래스 비율 * log_2(음성 클래스 비율) - 양성 클래스 비율 * log_2(양성 클래스 비율)

  • 이 때 minus sign을 붙이는 이유는 log 안의 값이 1보다 작으므로 음수가 나오기 때문이다.

Branching

In [14]:
dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_scaled, train_target)

print(dt.score(train_scaled, train_target))
print(dt.score(test_scaled, test_target))
0.8454877814123533
0.8415384615384616
In [15]:
plt.figure(figsize=(15,10))
plot_tree(dt, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()
  • 위 Tree를 보면 깊이 1의 node는 당도를 기준으로,
  • 깊이 2의 node에서 왼쪽은 도수를 기준으로, 오른쪽은 pH를 기준으로 분류를 했음을 알 수 있다.
  • feature 별로 scale이 다른데 표준화를 해야할까?
    • 불순도 계산시에는 어차피 비율로써 들어가기 때문에 표준화를 할 필요가 없다.
    • 이것이 DT의 또 다른 장점 중 하나이다.
In [16]:
dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_input, train_target)

print(dt.score(train_input, train_target))
print(dt.score(test_input, test_target))
0.8454877814123533
0.8415384615384616

전처리하기 전 데이터와 완벽히 정확도가 같음을 알 수 있다.

In [17]:
plt.figure(figsize=(20,15))
plot_tree(dt, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()
  • 역시 전처리 하기 전과 결과적으로 같은 그래프가 나온다.
    • 그러나 데이터를 전처리하지 않아 직관적인 해석이 가능하다.
    • 위에서 ss를 거친 경우 당도가 음수였는데, 이 경우는 기존 값을 사용하기 때문이다.
In [18]:
print(dt.feature_importances_)
[0.12345626 0.86862934 0.0079144 ]
  • DT의 featureimportances 속성에는 특성의 중요도가 저장되어 있다.
    • 이 경우 두번째 특성인 당도의 중요도가 87%정도로 높은 것을 알 수 있다.
    • 그러므로 깊이 1인 노드에서 당도를 기준으로 트리를 나눈 것이다.
    • 특성 중요도는 각 노드의 정보 이득과 전체 샘플에 대한 비율을 곱한 후 특성별로 더하여 계산한다.
    • 특성 중요도를 활용하여 결정 트리 모델을 특성 선택에 활용할 수 있다. 이것이 결정 트리 알고리즘의 또 다른 장점 중 하나이다.
  • 결정 트리는 많은 앙상블 학습 알고리즘의 기반이 된다.
    • 앙상블 학습은 신경망과 함께 가장 높은 성능을 내기 때문에 인기가 높은 알고리즘이다.

확인 문제

In [19]:
dt = DecisionTreeClassifier(min_impurity_decrease=0.0005, random_state=42)
dt.fit(train_input, train_target)

print(dt.score(train_input, train_target))
print(dt.score(test_input, test_target))
0.8874350586877044
0.8615384615384616
  • min_impurity_decrease 매개변수 :

어떤 노드의 정보 이득 * 노드의 샘플 수 /. 전체 샘플 수 값이 매개변수 값보다 작으면 더 이상 분할하지 않는다.

  • 성능은 depth를 3으로 설정했을 때보다 높고, 처음 했던 것처럼 depth 제한이 없는 경우처럼 과대적합하지도 않다.
In [20]:
plt.figure(figsize=(20,15), dpi=300)
plot_tree(dt, filled=True, feature_names=['alcohol', 'sugar', 'pH'])
plt.show()
  • Tree가 비대칭적으로 생겼음을 알 수 있다.
In [21]:
from IPython.core.display import display, HTML

display(HTML("<style>.container { width:90% !important; }</style>"))