혼자 공부하는 머신러닝 + 딥러닝 05-1 결정트리

손지호·2023년 8월 4일
0

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

import pandas as pd
wine = pd.read_csv('https://bit.ly/wine_csv_data')
# 처음 5개의 샘플 확인
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

→ 전체 와인 데이터에서 화이트 와인 골라내는 문제.

# info 메서드 : 데이터프레임의 각 열의 데이터 타입과 누락된 데이터 확인하는 데 유용.
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

+ 누락된 값 있으면???
누락된 값 있다면 그 데이터를 버리거나 평균값으로 채운 후 사용 가능. 어떤 방식이 최선인지는 미리 알기 어려움. 여기서도 항상 훈련 세트의 통계 값으로 테스트 세트 변환한다! 즉 훈련 세트의 평균값으로 테스트 세트의 누락된 값 채워야 한다.

# describe() : 열에 대한 간략한 통계 출력. 최소, 최대, 평균값 등 볼 수 있음.
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		1사분위수
50%	10.300000	3.000000	3.210000	1.000000		중앙값/2사분위수
75%	11.300000	8.100000	3.320000	1.000000		3사분위수
max	14.900000	65.800000	4.010000	1.000000		최대

→ 알코올 도수와 당도, pH 값의 스케일이 다르다는 것.
이전처럼 사이킷런 StandardScaler 클래스 사용해 특성 표준화.

# 넘파이 배열로 바꾸로 훈련 세트와 테스트 세트로 나누기
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) 
    # 실습과 결괏값 같도록 random_state를 42로 고정.

train_test_split() 함수는 설정값을 지정하지 않으면 25%를 테스트 세트로 지정. 샘플 개수가 충분히 많으면 20% 정도만 테스트 세트로 나눔. 코드의 test_size = 0.2가 이런 의미.

# 만들어진 훈련 세트와 테스트 세트의 크기 확인
# 훈련 세트 5,197개, 테스트 세트는 1,300개
print(train_input.shape, test_input.shape)
>>> (5197, 3) (1300, 3)
# 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

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


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

# 로지스틱 회귀가 학습한 계수와 절편 출력.
print(lr.coef_, lr.intercept_)
>>> [[ 0.51270274  1.6733911  -0.68767781]] [1.81777902]

하지만 보고서 작성했을 때, 로지스틱 회귀 모델 이해하기 힘듦. 그저 추측!!
대부분의 머신런이 모델은 학습의 결과 설명하기 어렵다.


결정트리

결정트리(Decision Tree) 모델이 이유를 설명하기 쉽다. 스무고개처럼 질문을 하나씩 던져서 정답 맞춰가는 것.
데이터를 잘 나눌 수 있는 질문을 찾는다면 계속 질문을 추가하여 분류 정확도를 높일 수 있음. DecisionTreeClassifier 클래스 사용해 결정 트리 모델 훈련.

+결정 트리 모델을 만들 때 왜 random_state 지정하나요?
사이킷런의 결정 트리 알고리즘은 노드에서 최적의 분할을 찾기 전에 특성의 순서를 섞는다. 따라서 약간의 무작위성이 주입되는데 실행할 때마다 점수가 조금씩 달라질 수 있기 때문. 여기서는 독자들이 실습한 결과와 책의 내용이 같도록 유지하기 위해 random_state 지정하지만, 실전에서는 필요 없음.

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


결정트리는 위에서부터 아래로 거꾸로 자라난다! 맨 위의 노드(node)를 루트 노드(root node)라 부르고 맨 아래 끝에 달린 노드를 리트 노드(leaf node)라 부른다. 맨 아래 끝에 달린 노드를 리프 노드(leaf node)라고 부른다.

+ 노드가 뭔데요??
결정 트리를 구성하는 핵심 요소. 노드는 훈련 데이터의 특성에 대한 테스트를 표현한다.

너무 복잡하니까 트리의 깊이를 제한해서 출력해보기. max_depth 매개변수를 1로 주면 루트 노드를 제외하고 하나의 노드를 더 확장하여 그려준다. 또 filled 매개변수에서 클래스에 맞게 노드의 색을 칠할 수 있다. feature_names 매개변수에는 특성의 이름을 전달할 수 있다. 이렇게 하면 노드가 어떤 특성으로 나뉘는지 좀 더 잘 이해할 수 있다.

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


→ 루트 노드는 당도(sugar)가 -0.239 이하인지 질문. 만약 어떤 샘플의 당도가 -0.239와 같거나 작으면 왼쪽 가지로 간다. 그렇지 않으면 오른쪽 가지로 이동. 즉, 왼쪽이 Yes, 오른쪽이 No. 루트 노드의 총 샘플 수 (samples)는 5,189개. 이 중 음성 클래스(레드 와인)는 1,258개, 양성 클래스(화이트 와인)는 3,939개. 이 값은 value에 나타나 있음.


→ 당도가 더 낮은지 물어봄. 당도가 -0.802와 같거나 낮다면 다시 왼쪽 가지로, 그렇지 않으면 오른쪽 가지로 이동. 이 노드에서 음성 클래스와 양성 클래스의 샘플 개수는 각각 1,177개와 1,745개. 루트 노드보다 양성 클래스, 즉 화이트 와인의 비율이 크게 줄어듦. 이유는 오른쪽 노드 보면 됨!


→ 음성 클래스가 81개, 양성 클래스가 2,194개로 대부분이 화이트 와인 샘플이 이 노드로 이동함! 노드의 바탕 색깔을 유심히 보면 루트 노드보다 이 노드가 더 진하고, 왼쪽 노드는 더 연하다. plot_tree() 함수에서 filled=True로 지정하면 클래스마다 색깔을 부여하고, 어떤 클래스의 비율이 높아지면 점점 진한 색으로 표시해준다!

결정트리에서 예측하는 방법은 간단함. 리프 노드에서 가장 많은 클래스가 예측 클래스가 된다. k-최근접 이웃과 매우 비슷!! 만약 이 결정 트리의 성장을 여기서 멈춘다면 왼쪽 노드에 도달한 샘플과 오른쪽 노드에 도달한 샘플은 모두 양성 클래스로 예측된다. 두 노드 모두 양성 클래스의 개수가 많기 때문!!!
+ 만약 결정 트리를 회귀 문제에 적용하려면 리프 노드에 도달한 샘플의 타깃을 평균하여 예측값으로 사용. 사이킷런의 결정 트리 회귀 모델은 DecisionTreeRegressor이다.


불순도

gini는 지니 불순도(Gini Impurity)를 의미. DecisionTreeClassifier 클래스의 criterion 매개변수의 기본값이 'gini'. criterion 매개변수의 용도는 노드에서 데이터를 분할할 기준을 정하는 것. 앞의 그린 트리에서 당도 -0.239를 기준으로 왼쪽과 오른쪽 노드로 나누는 것이 criterion 매개변수에 지정한 지니 불순도를 사용. 지니 불순도는 어떻게 계산할까??

지니 불순도는 클래스의 비율을 제곱해서 더한 다음 1에서 뺀다.
지니 불순도 = 1 - (음성 클래스 비율^2 + 양성 클래스 비율^2)
다중 클래스 문제라면 클래스가 더 많겠지만 계산하는 방법은 동일!!

루트 노드는 총 5,197개의 샘플이 있고 그중에 1,258개가 음성 클래스, 3,939개가 양성 클래스. 따라서 지니 불순도는
1 - ((1258 / 5197)^2 + (3939 / 5197)^2) = 0.367

결정 트리 모델은 부모 노드(parent node)와 자식 노드(child node)의 불순도 차이가 가능한 크도록 트리 성장시킴. 부모 노드와 자식 노드의 불순도 차이를 계산하는 방법은 먼저 자식 노드의 불순도를 샘플 개수에 비례하여 모두 더하고 부모 노드의 불순도에서 빼면 된다.

예를 들어 앞 트리 그림에서 루트 노드를 부모 노드라고 하면 왼쪽 노드와 오른쪽 노드가 자식 노드가 된다. 왼쪽 노드로는 2,922개의 샘플이 이동했고, 오른쪽 노드로는 2,275개의 샘플이 이동했다. 그럼 불순도의 차이는
부모 불순도 - (왼쪽 노드 샘플 수 / 부모의 샘플 수) x 왼쪽 노드 불순도 - (오른쪽 노드 샘플 수 / 부모의 샘플 수) x 오른쪽 노드 불순도 = 0.367 - (2922 / 5197) x 0.069 = 0.066

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

DecisionTreeClassifier 클래스에서 criterion = 'entropy'를 지정하여 엔트로피 불순도를 사용할 수 있다. 엔트로피 불순도도 노드의 클래스 비율을 사용하지만 지니 불순도처럼 제곱이 아니라 밑이 2인 로그를 사용하여 곱한다. 예를 들어 루트 노드의 엔트로피 불순도는
-음성 클래스 비율 x log_2(음성 클래스 비율) - 양성 클래스 비율 x log_2(양성 클래스 비율) = -(1258 / 5197) x log_2(1258 / 5197) - (3939 / 5197) x log_2(3939 / 5197) = 0.798
보통 기본값인 지니 불순도와 엔트로피 불순도가 만든 결과의 차이는 크지 않다. 여기서는 기본값인 지니 불순도를 계속 사용!

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

하지만 앞의 트리는 제한 없이 자라나서 훈련 세트보다 테스트 세트에서 점수가 크게 낮았다. 이제 이 문제 다루자!!


가지치기

결정 트리도 가지치기 해야한다! 그렇지 않으면 무작정 끝까지 자라나는 트리가 만들어진다. 훈련 세트에는 아주 잘 맞겠지만 테스트 세트에서 점수는 그에 못 미칠 것.

결정 트리에서 가지치기를 하는 가장 간단한 방법을 자라날 수 있는 트리의 최대 깊이를 지정하는 것. DecisionTreeClassifier 클래스의 max_depth 매개변수를 3으로 지정하여 모델을 만들어 보자. 이렇게 하면 루트 노드 아래로 최대 3개의 노드까지만 성장!

# 훈련 세트의 성능은 낮아졌지만 테스트 세트의 성능은 거의 그대로.
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()


맨 위 하나만 있는 노드가 '깊이 0', 두 개 노드가 '깊이 1', 노드 4개가 '깊이 2', 마지막 노드 8개가 '깊이 3'

루트 노드 다음에 있는 깊이 1의 노드는 모두 당도(sugar)를 기준으로 훈련 세트를 나눈다. 하지만 깊이 2의 노드는 맨 왼쪽의 노드만 당도를 기준으로 나누고 왼쪽에서 두 번째 노드는 알코올 도수(alcohol)를 기준으로 나누다. 오른쪽 두 노드는 pH를 사용.
깊이 3에 있는 노드가 최종 노드인 리프 노드. 왼쪽에서 세 번째에 있는 노드만 음성 클래스가 더 많음. 이 노드에 도착해야만 레드 와인으로 예측. 그럼 루트 노드부터 이 노드까지 도달하려면 당도는 -0.239보다 작고 또 -0.802보다 커야 한다. 그리고 알코올 도수는 0.454보다 작아야 한다. 그럼 세 번째 리프 노드에 도달. 즉, 당도가 -0.802보다 크고 -0.239보다 작은 와인 중에 알코올 도수가 0.454와 같거나 작은 것이 레드 와인.

-0.802라는 음수로 된 당도를 어떻게 설명해야 할까??? 샘플을 어떤 클래스 비율로 나누는지 계산할 때 특성값의 스케일이 계산에 영향을 미치지 않는다! 특성값의 스케일은 결정 트리 알고리즘에 아무런 영향 미치지 않는다. 따라서 표준화 전처리를 할 필요가 없다!! 이것이 결정 트리 알고리즘의 또 다른 장점 중 하나.

# 전처리하기 전 훈련 세트(train_input)와 테스트 세트(test_input)로 결정 트리 모델 다시 훈련
# 정확히 같다!!
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와 같거나 작은 것이 레드 와인. 그 외에는 모두 화이트 와인으로 예측!

마지막으로 결정 트리는 어떤 특성이 가장 유용한지 나타내는 특성 중요도를 계산해준다. 이 트리의 루트 노드와 깊이 1에서 당도를 사용했기 때문에 아마도 당도(sugar)가 가장 유용한 특성 중 하나일 것. 특성 중요도는 걸정 트리 모델의 featureimportances 속송에 저장되어 있다!

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

두 번째 특성인 당도가 0.87 정도로 특성 중요도가 가장 높음! 그 다음 알코올 도수, pH 순. 이 값 모두 더하면 1이 된다. 특성 중요도는 각 노드의 정보 이득과 전체 샘플에 대한 비율을 곱한 후 특성별로 더하여 계산. 트성 중요도를 확용하면 결정 트리 모델을 특성 선택에 활용할 수 있다. 이 것이 결정 트리 알고리즘의 또 다른 장점 중 하나!




전체 코드 (출처 : https://bit.ly/hg-05-1)

import pandas as pd

wine = pd.read_csv('https://bit.ly/wine_csv_data')

wine.head()
wine.info()
wine.describe()

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

print(lr.coef_, lr.intercept_)

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

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

plt.figure(figsize=(10,7))
plot_tree(dt, max_depth=1, 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_input, train_target))
print(dt.score(test_input, test_target))

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

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

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

정리

  • 결정트리는 예/아니오에 대한 질문을 이어나가면서 정답을 찾아 학습하는 알고리즘입니다. 비교적 예측 과정을 이해하기 쉽고 성능도 뛰어나다.
  • 불순도는 결정 트리가 최적의 질문을 찾기 위한 기준. 사이킷런은 지니 불순도와 엔트로피 불순도를 제공.
  • 정보이득은 부모 노드와 자식 노드의 불순도 차이. 결정 트리 알고리즘은 정보 이득이 최대화되도록 학습한다.
  • 결정 트리는 제한 없이 성장하면 훈련 세트에 과대적합 되기 쉬움. 가지치기는 결정 트리의 성자을 제한하는 방법. 사이킷런의 결정 트리 알고리즘은 여러 가지 가지치기 매개변수를 제공.
  • 특성 중요도는 결정 트리에 사용된 특성이 불순도를 감소하는데 기여한 정도를 나타내는 값. 특성 중요도를 계산할 수 있는 것이 결정 트리의 또 다른 큰 장점.

핵심 패키지와 함수

pandas

  • info() : 데이터프레임의 요약된 정보를 출력.
    인덱스와 컬럼 타입을 출력하고 널(null)이 아닌 값의 개수, 메모리 사용량을 제공함. verbose 매개변수의 기본값 True를 False로 바꾸면 각 열에 대한 정보를 출력하지 않음.
  • describe() : 데이터프레임 열의 통계 값을 제공. 수치형일 경우 최소, 최대, 평균, 표준편차와 사분위값 등이 출력됨.
    문자열 같은 객체 타입의 열은 가장 자주 등장하는 값과 횟수 등이 출력됨.
    percentiles 매개변수에서 백분위수를 지정. 기본값은 [0.25, 0.3 0.75]

scikit-learn

  • DecisionTreeClassifier : 결정 트리 분류 클래스.
    criterion 매개변수는 불순도를 지정하며 기본값은 지니 불순도를 의미하는 'gini'이고 'entropy'를 선택하여 엔트로피 불순도를 사용할 수 있다.
    splitter 매개변수는 노드를 분할하는 전략을 선택. 기본값은 'best'로 정보 이득이 최대가 되도록 분할. 'random'이면 임의로 노드를 분할한다.
    max_depth는 트리가 성장할 최대 깊이를 지정한다. 기본값은 None으로 리프 노드가 순수하거나 min_samples_split보다 샘플 개수가 적을 때까지 성장한다.
    min_samples_split는 노드를 나누기 위한 최소 샘플 개수. 기본값은 2.
    max_features 매개변수는 최적의 분할을 위해 탐색할 특성의 개수를 지정. 기본값은 None으로 모든 특성을 사용함.

  • plot_tree() : 결정 트리 모델을 시각화. 첫 번째 매개변수로 결정 트리 모델 객체를 전달함.
    max_depth 매개변수로 나타낼 트리의 깊이를 지정함. 기본값은 None으로 모든 노드를 출력함.
    feature_names 매개변수로 특성의 이름을 지정할 수 있음.
    filled 매개변수를 True로 지정하면 타깃값에 따라 노드 안에 색을 채움.

profile
초보 중의 초보. 열심히 하고자 하는 햄스터!

0개의 댓글