Machine Learning/Basic

[5-1] 결정 트리

jwjwvison 2021. 4. 7. 19:28

이 포스팅은 혼자 공부하는 머신러닝 + 딥러닝 책을 공부하고 정리한것 입니다.


 와인을 판매하는 상황을 가정해보자. 와인 종류에는 레드 와인과 화이트 와인이 있다. 그런데 와인의 표시가 누락되어있고 캔에 인쇄된 알코올 도수, 당도 , pH 값으로 와인 종류를 구별해야 한다.

 

 

로지스틱 회귀로 와인 분류하기

import pandas as pd

wine=pd.read_csv('https://bit.ly/wine-date')
wine.head()

0:  레드, 1: 화이트

 레드 와인과 화이트 와인을 구분하는 이진 분류 문제이고, 화이트 와인이 양성 클래스 이다. 즉 전체 와인 데이터에서 화이트 와인을 골라내는 문제가 되었다.

 

wine.info()

 출력 결과를 보면 총 6497개의 샘플이 있고 4개의 열은 모두 실숫값이다. Non-Null Count 가 모두 6497이므로 누락된 값은 없는것 같다.

 

wine.describe()

평균, 표준편차, 최소, 1사분위수, 중간값(2사분위수), 3사분위수, 최대

 

 여기서 알수 있는 것은 pH 값의 스케일이 다르다는 것이다.

data=wine[['alcohol','sugar','pH']].to_numpy()
target=wine['class'].to_numpy()

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)
print(train_input.shape,test_input.shape)

 

from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression

ss=StandardScaler()
ss.fit(train_input)
train_scaled=ss.transform(train_input)
test_scaled=ss.transform(test_input)

lr=LogisticRegression()
lr.fit(train_scaled,train_target)
print(lr.score(train_scaled,train_target))
print(lr.score(test_scaled,test_target))

 점수가 높지 않다. 생각보다 화이트 와인을 골라내는게 어려운것 같다. 훈련 세트와 테스트 세트의 점수가 모두 낮으니 모델이 다소 과소접합 된것 같다. 

 

 이 모델을 설명하기 위해 로지스틱 회귀가 학습한 계수와 절편을 출력해 보자

print(lr.coef_,lr.intercept_)

 풀어서 말하면 이 모델은 알코올 도수 값에 0.512를 곱하고 당도에 1.67을... 이런식이다. 사실 우리는 이 모델이 왜 저런 계수 값을 학습했는지 정확히 이해하기 어렵다. 아마도 알코올 도수와 당도가 높을수록 화이트 와인일 가능성이 높고, pH가 높을수록 레드 와인일 가능성이 높은 것 같다. 하지만 이 숫자가 어떤 의미인지 설명하긴 어렵다. 더군다나 다항 특성을 추가하다면 설명하기가 더 어려울 것이다.

 

 

결정 트리

 결정트리(Decision Tree)는 모델이 이유를 설명하기 쉽다 데이터를 잘 나눌 수 있는 질문을 찾는다면 계속 질문을 추가해서 분류 정확도를 높일 수 있다. 사이킷런의 DecisionTreeClassifier 클래스를 사용해 결정 트리 모델을 훈련해 보자. 새로운 클래스이지만 사용법은 이전과 동일하다. fit() 메서드를 호출해서 모델을 훈련한 다음 score() 메서드로 정확도를 평가해 본다.

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))

과대적합 모델

 

사이킷런은 plot_tree() 함수를 사용해 결정 트리를 이해하기 쉬운 트리 그림으로 출력해 준다.

import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

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

맨 위의 노드를 루트노드(root node) 라고 부르고 맨 아래 끝에 달린 노드를 리프 노드(leaf node) 라고 한다.

 

너무 복잡하니 plot_tree() 함수에서 트리의 깊이를 제한해서 출력해 보자. max_depth 매개변수를 1로 주면 루트 노드를 제외하고 하나의 노드를 더 확장하여 그린다.

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

기본적으로 그림이 담고 있는 정보는 다음과 같다.

 

 

 루트 노드는 당도(sugar)가 -0.239 이하인지 질문을 한다. 만약 어떤 샘플의 당도가 -0.239와 같거나 작으면 왼쪽 가지로 간다. 그렇지 않으면 오른쪽 가지로 이동한다. 즉 왼쪽이 yes, 오른쪽이 No이다. 

 루트 노드의 총 샘플 수(samples) 는 5197개이다. 이 중에서 음성 클래스(레드 와인)은 1258개 이고, 양성 클래스(화이트 와인) 은 3939개 이다. 이 값이 value에 나타나 있다.

 plot_tree() 함수에서 filled=True로 지정하면 클래스마다 색깔을 부여하고, 어떤 클래스의 비율이 높아지면 점점 진한색으로 표시한다.

 

 결정 트리에서 예측하는 방법은 간단하다. 리프 노드에서 가장 많은 클래스가 예측 클래스가 된다. 앞에서 보았던 k-최근접 이웃과 매우 비슷하다. 만약 이 결정 트리의 성장을 여기서 멈춘다면 왼쪽 노드에 도달한 샘플과 오른쪽 노드에 도달한 샘플은 모두 양성 클래스로 예측된다. 두 노드 모두 양성 클래스의 개수가 많기 때문이다.

 

 만약 결정 트리를 회귀 문제에 적용하면 리프 노드에 도달한 샘플의 타깃을 평균하여 예측값으로 사용한다. 사이킷런의 결정 트리 회귀모델은 DecisionTreeRegressor 이다.

 

gini는 지니 불순도(Gini impurity)를 의미한다. DecisionTreeClassifier 클래스의 criterion 매개변수의 기본값이 'gini' 이다. criterion 매개변수의 용도는 노드에서 데이터를 분할할 기준을 정하는 것이다. 앞의 그린 트리에서 루트노드가 당도 -0.239를 기준으로 왼쪽 오른쪽 노드로 나눈 방법은 criterion 매개변수에 지정한 지니 불순도를 사용한다.

 

 지니 불순도는 클래스의 비율을 제곱해서 더한 다음 1에서 빼면 된다.

 만약 100개의 샘플이 있는 어떤 노드의 두 클래스 비율이 정확히 1/2씩 이라면 지니 불순도는 0.5가 되어 최악이 된다.

 

 노드에 하나의 클래스만 있다면 지니 불순도는 0이 되어 가장 작다. 이런 노드를 순수 노드라고 부른다.

 

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

 이런 부모와 지식 노드 사이의 불순도 차이를 정보이득(information gain) 이라고 부른다. 이 알고리즘은 정보 이득이 최대가 되도록 데이터를 나눈다.

 

DecisionTreeClassifier 클래스에서 criterion='entropy'를 지정하여 엔트로피 불순도를 사용할 수 있다. 엔트로피 불순도도 노드의 클래스 비율을 사용하지만 지니 불순도처럼 제곱이 아니라 밑이 2인 로그를 사용하여 곱한다.

 노드를 순수하게 나눌수록 정보 이득이 커진다. 새로운 샘플에 대해 예측할 때에는 노드의 질문에 따라 트리를 이동한다. 그리고 마지막에 도달한 노드의 클래스 비율을 보고 예측을 만든다. 그런데 앞의 트리는 제한없이 자라났기 때문에 훈련 세트보다 테스트 세트에서 점수가 크게 낮았다.

 

 트리를 가지치기를 해 줘야 하는데 그렇지 않으면 무작정 끝까지 자라나는 트리가 만들어져서 훈련 세트에는 잘 맞겠지만 테스트 세트에서 점수는 그에 못 미치게 된다. 이를 두고 일반화가 잘 안 될것 같다고 말한다. 결정 트리에서 가지치기를 하는 가장 간단한 방법은 자라날 수 있는 트리의 최대 깊이를 지정하는 것이다.

 

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))

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

 깊이 3에 있는 노드가 최종 노드인 리프 노드이다. 왼쪽에서 세번째에 있는 노드만 음성 클래스가 더 많다. 이 노드에 도착해야만 레드 와인을 예측한다. 그럼 루트 노드부터 이 노드까지 도달하려면 당도는 -0.239 보다 작도 또 -0.802 보다 작아야 한다. 그리고 알코올 도수는 0.454 보다 작아야 한다. 즉 당도가 -0.802와 같거나 작은 와인 중에 알코올 도수가 0.454와 같거나 작은 것이 레드 와인이다.

 

 그런데 -0.802라는 음수로 된 당도를 어떻게 설명해야 할까? 앞서 불순도를 기준으로 샘플을 나눈다고 했는데 불순도는 클래스별 비율이다. 그러므로 특성값의 스케일이 계산에 영향을 미치지 않는다. 따라서 표준화 전처리를 할 필요가 없다. 이것이 결정 트리 알고리즘의 또 다른 장점 중 하나이다.

 

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))

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

당도가 1.625와 같거나 작은 와인 중에 알코올 도수가 11.025와 같거나 작은 것이 레드 와인이다.

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

print(dt.feature_importances_)

 두번째 특성인 당도가 0.87 정도로 특성 중요도가 가장 높다. 특성 중요도를 활용하면 결정 트리 모델을 특성 선택에 활용할 수 있다.

 

 

 

결론


 이번 머신러닝 모델은 알코올 도수, 당도, pH 데이터를 기준으로 화이트 와인을 골라내는 이진 분류 로지스틱 회귀 모델을 훈련했다. 그 이후 결정 트리를 사용해서 분류하는 문제를 풀었다. 결정 트리는 비교적 비전문가에게도 설명하기 쉬운 모델을 만든다. 결정트리는 많은 앙상블 학습 알고리즘의 기반이 된다. 앙상블 학습은 신경망과 함께 가장 높은 성능을 내기 때문에 인기가 높은 알고리즘 이다.