[혼공머신] 5-1장 결정 트리

Changh2·2024년 11월 10일
0

[혼자 공부하는 머신러닝+딥러닝] 교재 5장을 기반으로 작성되었습니다.


신상품으로 캔 와인을 판매하는데,
알코올 도수, 당도, PH 값으로 화이트 와인인지 레드 와인인지 종류를 구별하려고 한다.

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

이전에 배운 로지스틱 회귀로 문제를 해결해보자.

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

# 📌 샘플 확인
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

# class는 타깃값으로, 0이면 레드 와인, 1이면 화이트 와인을 뜻한다.
# (양성 클래스인 화이트 와인을 골라내는 이진 분류 문제)

모델을 훈련하기 전에, 판다스의 유용한 메서드 2개로 데이터셋을 확인해보자.

info()

판다스의 유용한 메서드 중 하나.
데이터프레임의 요약된 정보를 출력하는데, 각 열의 데이터 타입과 누락된 데이터가 있는지 확인하는 데 유용하다.

describe()

판다스의 유용한 메서드 중 하나.
데이터프레임 열의 통계 값을 제공하는데,
수치형일 경우 최소, 최대, 평균, 표준편차, 사분위값 등이 출력되고,
문자열 같은 객체 타입의 열의 가장 자주 등장하는 값과 횟수 등이 출력된다.

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
    
# 총 6,497개의 샘플이 있고 4개의 열은 모두 실숫값이다.
# Non-Null Count가 모두 6497이므로 누락된 값은 없다.
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

# 알코올 도수와 당도, 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)
	# 샘플 개수가 충분치 않으므로 20% 정도만 테스트 세트로 나눠준다. (test_size=0.2)
    
print(train_input.shape, test_input.shape)  # 훈련, 테스트 세트의 크기 확인
>>> (5197, 3) (1300, 3)
# 📌 표준화 전처리
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
ss.fit(train_input)
train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)
# 📌 로지스틱 회귀 모델 훈련
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

훈련세트와 테스트세트의 결과 점수가 모두 낮으니 과소적합된거 같다.
우선 결과를 보고하자.


설명하기 쉬운 모델과 어려운 모델

결과를 제출할 보고서를 만드려는데, 이 모델을 설명하기 위해 이 모델이 학습한 계수와 절편을 출력하자.

print(lr.coef_, lr.intercept_)
>>> [[ 0.51268071  1.67335441 -0.68775646]] [1.81773456]

이해하기가 너무 어렵다....
이해하기 쉽도록 "순서도"처럼 쉽게 설명해서 가져오라 하는데,
이렇게 쉽게 설명할 수 있는 모델이 있을까?


결정 트리

결정 트리예 / 아니오에 대한 질문을 이어나가면서 정답을 찾아 학습하는 알고리즘으로,
비교적 예측 과정을 이해하기 쉽고 성능도 뛰어나다.
사이킷런의 DecisionTreeClassifier 클래스로 제공된다.

# 📌 결정트리 모델 훈련+평가
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

# 훈련세트에 비해 테스트세트 점수가 낮다. (과대적합)

이 모델을 그림으로 표현하기 위해 plot_tree() 함수를 사용해보자.

뭔가 엄청난 트리가 만들어졌다... 너무 복잡하니 max_depth 매개변수를 1로 조절해서 그려보자.

기본적으로 이 그림이 담고 있는 정보는 아래와 같다.

위의 그림에서 루트 노드를 살펴보자.

루트 노드의 총 샘플 수 는 5197개 이고, 
이 샘플 중 음성 클래스(레드와인)는 1258개, 양성 클래스(화이트와인)는 3939개이다.
sugar가 -0.239와 같거나 작으면 왼쪽 가지로 가고, -0.239보다 크면 오른쪽 가지로 간다.

이런 식으로 리프 노드까지 도달한 후, 리프 노드에서 가장 많은 클래스가 예측 클래스가 된다.

근데, 그림에서 gini는 뭘 뜻하는 걸까?


불순도

gini는 지니 불순도를 의미하는데, 결정트리 클래스의 criterion 매개변수 기본값이 'gini'이다.
(criterion은 노드에서 데이터를 분할할 기준을 정하는 기준이다.)
즉, 위에서 루트 노드는 '어떻게 당도 -0.239를 기준으로' 노드를 나눴는지 정하는 것이다.
지니 불순도를 계산하는 방법은 아래와 같다.

위의 루트 노드를 예시로 계산해보면 아래와 같다.

만약 100개의 샘플이 음성 클래스 50개, 양성 클래스 50개로 구성되어있다면
지니 불순도는 0.5가 되어 최악이 된다.
노드에 하나의 클래스만 있다면, (100개의 샘플중 음성 클래스는 0개, 양성 클래스만 100개)
지니 불순도는 0이 되어 가장 작고, 이런 노드를 순수 노드라고 부른다.

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

이런 부모와 자식 노드 사이의 불순도 차이정보 이득이라고 부르고, 아래와 같이 계산된다.

+참고)
불순도는 gini 뿐만 아니라 entropy, 즉 엔트로피 불순도도 있는데,
지니 불순도의 '제곱 방식' 대신에 로그를 사용하여 곱한다.


가지치기

위의 결정트리 모델은 훈련세트보다 테스트세트에서 점수가 낮았는데,
이는 트리가 제한 없이 자라났기 때문에 과대적합된 것이라 볼 수 있다.

실제 나무와 같이 '가지치기'를 해줘야 하는데, 트리의 최대깊이를 지정하는 것으로 쉽게 조절할 수 있다.

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
    
# 테스트 세트의 성능은 거의 그대로다...  다음장에서 더 알아보자

이 모델의 트리 그래프를 plot_tree() 함수로 그려보자

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

각 리프 노드에서 가장 많은 클래스는 양성클래스(화이트와인)이므로 예측 클래스는 화이트와인이다.


앞서, 한 노드에서 불순도를 기준으로 샘플을 나눈다고 했고, 불순도는 클래스별 비율을 가지고 계산했다.
이는 각 특성의 스케일이 샘플을 나눌때 영향을 주지 않는다는 것을 의미하고,
결정 트리 모델은 표준화 전처리를 할 필요가 없다는 걸 뜻한다.

# 📌 python표준화가 안 된 데이터로 훈련
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()

트리는 같지만, 특성값을 표준점수로 바꾸지 않아 이해하기가 훨씬 쉽다.


마지막으로, 어떤 특성이 가장 유용한지 나타내는 특성 중요도를 알아보자.
이는 feature_importances_ 속성에 저장되어 있다.

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

# 두번째 특성인 당도가 0.87 정도로 가장 높은걸 확인할 수 있다.

특성 중요도를 활용하면 결정 트리 모델을 특성 선택에 활용할 수 있다.

profile
Shoot for the moon!

0개의 댓글