본문 바로가기

머신러닝 + 딥러닝

[혼공머신] CHAPTER 05-1 결정 트리

728x90

이번 챕터는 레드와인과 화이트와인을 구분하는 문제이다.

알코올 도수, 당도, pH 값으로 와인 종류를 구분해보아야한다.

 

먼저, 로지스틱 회귀로 와인을 분류해보자!

import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
wine.head() # wine.tail()

 

첫 번째 열은 각각 알코올 도수, 당도, pH 값을 나타낸다.

class는 타깃값으로 0이면 레드와인, 1이면 화이트와인이다.

레드와인과 화이트와인을 구분하는 이진 분류 문제이고 화이트 와인이 양성 클래스이다.

 

판다스 데이터프레임의 유용한 메서드를 알아보자

wine.info()

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개의 샘플이 있고 4개의 열은 모두 실수값(float)임을 알 수 있다.

non-null count가 모두 6497이므로 누락된 값이 없어보인다.

 

각 열 별 NaN(결측값)을 확인할 수 있다.

만약 pH 열에 NaN값이 1개가 있다면 6496 non-null 이런 식으로 나올 것

 

wine.head().isnull()

isnull() 메서드로 어떤 위치에 결측값이 있는지 확인 가능

 

누락된 값이 있으면 True로 나오는데 그 데이터를 버리거나 평균값으로 채운 후 사용 가능

-> 훈련세트의 평균값으로 테스트세트의 누락된 값을 채워야한다

관련링크: https://dsbook.tistory.com/21 

 

wine.describe()

describe() : 열에 대한 간략한 통계를 출력해준다. 최소, 최대, 평균값 등

 

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

25%, 50%, 70% : 백분위수의 각 지점으로, 분포를 반영해 평균을 보완하는 목적으로 사용

제 1사분위수(Q1) 데이터의 25%가 이 값보다 작거나 같다
제 2사분위수(Q2) 중위수, 데이터의 50%가 이 값보다 작거나 같다
제 3사분위수(Q3) 데이터의 75%가 이 값보다 작거나 같다

 

특정 열의 요약 정보를 확인할 경우,

wine['alcohol'].describe()

 

-> 알코올도수, 당도, pH값의 스케일이 다르다는 것을 알 수 있다

 

판다스 데이터프레임을 넘파이 배열로 바꾸고 훈련세트와 테스트세트로 바꾼다.

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

data(입력데이터) : alcohol, sugar, pH

target(타깃데이터) : class

 

훈련세트와 테스트세트로 나누어본다.

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)

test_size=0.2 : train_test_split() 함수는 설정값을 지정하지 않으면 25%를 테스트세트로 지정한다. 샘플 개수가 충분히 많아 20% 정도만 테스트세트로 나누었다

 

print(train_input.shape, test_input.shape)

만들어진 테스트세트와 훈련세트의 크기를 알아보니

(5197, 3) (1300, 3)

훈련세트는 5197개이고 테스트세트는 1300개이다

 

StandardScaler 클래스를 사용해 훈련세트를 전처리한다

from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

 

표준점수로 변환된 train_scaled와 test_scaled를 사용해 로지스틱 회귀모델을 훈련한다.

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

과소적합이 된 것 같아 보인다. 

>>C값 변경, solver 매개변수에서 다른 알고리즘 선택, 다항 특성을 만들어 추가

 

print(lr.coef_, lr.intercept_)

계수와 절편 확인

[[ 0.51270274  1.6733911  -0.68767781]] [1.81777902]

-> 아마도! 알코올 도수와 당도가 높을수록 화이트와인일 가능성이 높고, pH가 높을수록 레드와인일 가능성이 높은 것 같다 -> 이 숫자가 정확히 어떤 의미인지 설명하기 어려울 것 -> 대부분 머신러닝 모델은 결과를 설명하기 어렵다

 


결정 트리(Decision Tree) : 의사결정 규칙을 나무 구조로 나타내어 전체 자료를 몇 개의 작은 집단으로 나누어서 분석하는 기법

- 분류 및 회귀가 가능한 머신러닝 알고리즘

- 스무고개처럼 질문을 하나씩 던져서 정답과 맞춰가는 것

 

사이킷런의 DecisionTreeClassifier 클래스를 사용해 결정트리 모델을 훈련해보자

from sklearn.tree import DecisionTreeClassifier
dt = DecisionTreeClassifier(random_state=42) #random_state=42를 없애도 차이가 많이 나지 않는다. 0.996921 / 0.8546153
dt.fit(train_scaled, train_target)
print(dt.score(train_scaled, train_target))
print(dt.score(test_scaled, test_target))

 

0.996921300750433
0.8592307692307692

훈련세트에 대한 점수가 거의 만점에 가깝지만 테스트세트의 성능은 그에 비해 낮아 과대적합이 되었다고 볼 수 있다

 

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

사이킷런의 plot_tree() 함수를 사용해 결정트리 그림을 나타내준다

루트 노드(root node) : 맨 위에 있는 노드

리프 노드(leaf node) : 맨 아래 끝에 달린 노드

 

max_depth 매개변수로 트리의 깊이를 제한해서 출력해본다.

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

 

max_depth 트리 표현의 최대 깊이, None이면 완전히 트리가 생성
filled 클래스마다 색깔이 부여되고, 어떤 클래스의 비율이 높아지면 점점 진한 색으로 표시(default=False)
feature_names 각 특성의 이름, 노드가 어떤 특성으로 나뉘는지 이해 가능

max_depth를 1로 주면 루트 노드를 제외하고 하나의 노드를 더 확장하여 그린다.

불순도(impurity) : 복잡성을 의미하며 어떤 범주 안에 서로 다른 데이터가 얼마나 섞여 있는지를 뜻한다.

 

DecisionTreeClassfier

DecisionTreeClassifier(criterion, splitter, max_depth, min_samples_split, min_samples_leaf, min_weight_fraction_leaf, max_features, random_state, max_leaf_nodes, 
min_impurity_decrease, min_impurity_split, class_weight, presort)

DecisionTreeClassfier 클래스의 criterion 매개변수의 기본값이 'gini'이다.

- criterion : 노드에서 데이터를 분할할 기준을 정하는 것

 

gini, 지니 불순도(Gini impurity)

- 지니 불순도의 최대값은 0.5

- 어떤 노드의 두 클래스가 정확히 1/2이라면 지니 불순도는 0.5가 되어 최악이 된다

- 노드에 하나의 클래스만 있다면 지니 불순도는 0이 되어 가장 작다 : 순수노드

 

지니 불순도 = 1 - (음성클래스 비율^2 + 양성클래스 비율^2)

 

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

 

  • 부모 노드와 자식노드의 불순도 차이 계산 방법 : 

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

 

 

정보 이득(information gain) : 부모와 자식 노드 사이의 불순도 차이

-> 지니 불순도를 기준으로 이 알고리즘은 정보 이득이 최대가 되도록 데이터를 나눈다

 

 

엔트로피 불순도

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

 

 

i) 루트 노드

당도(sugar)가 -0.239보다 작은지 알아본다. 샘플의 당도가 -0.239보다 같거나 작으면 왼쪽 가지로 가고 그렇지 않으면 오른쪽 가지로 이동한다.

루트노드의 총 샘플 수는 5197개이고 음성클래스는 1258개이고 양성클래스는 3939개이다.

 

  • 루트노드의 지니 불순도

1 - ((1258/5197)^2 + (3939/5197)^2) = 0.367

  • 루트노드의 엔트로피 불순도

-(1258/5197) * log_2(1258/5197) - (3939/5197) * log_2(3939/5197) = 0.798

 

ii) 왼쪽 노드

당도가 -0.802보다 낮은지를 묻고 이와 같거나 작다면 왼쪽 가지로, 그렇지 않으면 오른쪽 가지로 이동한다.

이 노드에서 음성클래스는 1177개, 양성클래스는 1745개이다.

 

iii) 오른쪽 노드

오른쪽 노드는 당도가 0.204 이하인지 물어본다.

음성클래스가 81개, 양성클래스가 2194개로 대부분의 화이트와인(양성클래스) 샘플들이 이동한 것을 알 수 있다.

 

  • 루트노드와 오른쪽, 왼쪽노드의 불순도 차이

0.367 - (2922/5197) * 0.481 - (2275/5197) * 0.069 = 0.066

 

결정트리에서는 리프노드에서 가장 많은 클래스가 예측 클래스가 된다. 여기서 왼쪽 노드와 오른쪽 노드는 둘 다 양성클래스(화이트 와인)로 예측이 된다.

 

∴ 불순도 기준을 사용해 정보 이득이 최대가 되도록 노드를 분할한다. 노드를 순수하게 나눌수록 정보 이득이 커진다. 새로운 샘플에 대해 예측할 때는 노드의 질문에 따라 트리를 이동한다. 마지막에 도달한 노드의 클래스 비율을 보고 예측을 만든다

 

 

→ 가지치기

- 결정트리의 성장을 제한하는 방법

- 결정트리가 제한 없이 성장하면 훈련세트에 과대적합되기 때문에 필요

 

결정트리에서 DecisionTreeClassifier 클래스의 max_depth 매개변수로 트리의 최대 깊이를 지정한다.

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

여기서 max_depth를 3으로 지정했다.

max_depth값이 클수록 모델의 복잡도가 올라간다.

0.8454877814123533
0.8415384615384616

훈련세트의 성능은 낮아졌지만 테스트세트의 성능은 그대로이다.

 

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

 

루트노드 다음에 있는 깊이 1의 노드는 당도(sugar)를 기준으로 훈련세트를 나눈다.

깊이 2의 노드는 당도, 알코올 도수(alcohol), pH값을 기준으로 나눈다.

깊이 3인 노드는 최종 노드인 리프노드이다. 주황색으로 된 노드만이 음성클래스가 더 많다. 즉, 이 노드까지 도착을 해야만 레드와인으로 예측한다.

-> 당도가 -0.802보다 크고 -0.239보다 작은 와인들 중에 도수가 0.454와 같거나 작은 것이 레드와인이다.

 

 

DecisionTreeClassfier(결정트리 알고리즘)

○ 특성값의 스케일이 계산에 전혀 영향을 미치지 않는다, 표준화 전처리 과정이 필요 없다.

○ 시각화 할 수 있다.

△ 끝까지 학습하면 과대적합이 일어난다.

 

전처리하기 전의 훈련세트와 테스트세트로 결정트리모델을 다시 훈련한다.

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

 

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

표준화 전처리를 하지 않은 모델이라 해석하기가 쉬워졌다.

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

 

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

print(dt.feature_importances_)

이 중요도는 결정 트리 모델의 feature_importances_ 속성에 저장되어 있다.

[0.12345626 0.86862934 0.0079144 ]

당도 > 알코올 도수 > pH 순으로 중요도가 높음을 알 수 있다.

이 값을 모두 더하면 1이 된다.

특성 중요도는 각 노드의 정보이득과 전체 샘플에 대한 비율을 곱한 후 특성별로 더하여 계산한다 -> 이를 활용하면 결정 트리 모델을 특성 선택에 활용 가능하다.

 

 

728x90