머신러닝(AI학습 33)

이유진·2024년 7월 8일

--15.결정트리(Decision Tree).ipynb--

결정트리(Decision Tree)

데이터 준비

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import os

base_path = r'/content/drive/MyDrive/dataset'

와인 분류 문제

알코올 도수, 당도, ph 값으로 와인의 종류 '분류'하기

"""
데이터 : Red Wine Quality

출처 : https://www.kaggle.com/datasets/uciml/red-wine-quality-cortez-et-al-2009

alcohol : 알코올 도수
sugar : 당도
pH : pH

class : 타깃값! 0 이면 레드와인, 1 이면 화이트 와인 <= 이진분류문제!

화이트와인이 양성클래스. 전체와인 데이터에서 화이트와인을 골라내는 문제다!

"""
None

file_path = os.path.join(base_path, 'wine.csv')
wine_df = pd.read_csv(file_path)
wine_df

wine_df.info()

↓ 6497개의 결측치 없는 데이터

wine_df.describe()

"""
balanced vs imbalanced 여부

suger는 뭔가 imbalanced 해보인다.
데이터는 양성클래스가 더 많은 것 같다.

alcohol, suger, ph 모두 스케일이 다르다 -> 전처리 필요
"""
None

wine_df.columns

numpy 배열로 변환

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

data

target

np.unique(target) # 반드시 확인해야함!!

훈련세트와 테스트세트 나누기

from sklearn.model_selection import train_test_split

train:test = 8:2로 분리

train_input, test_input, train_target, test_target = \
train_test_split(data, target, test_size = 0.2, random_state=42)

train_input.shape, test_input.shape

train_target.shape, test_target.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))

"""
↑ 둘다 점수가 높지 않다 -> 과소적합(underfit) 의심
"""
None

학습된 계수와 절편을 확인

print(lr.coef, lr.intercept)

"""
z = 0.51270274 x 알코올 + 1.6733911 x 당도 + (-0.68767781) x ph + 1.81777902

z 값이 0보다 크면 화이트 와인(양성), 작으면 레드와인(음성)

현재 정확도는 77% 정도
"""
None

과연 이 모델을 설명해주면, 듣는 사람들이 쉽게 이해할 수 있나?

"""
대부분의 머신러닝의 모델은 학습의 결과를 설명하기 어렵다.

'순서도 (flow chart)' 처럼 알기 쉽게 설명이 되는 모델은 없을까?
"""
None

DecisionTreeClassifier

결정트리

"""
결정트리는 "이유를 설명하기 쉽다"

질문을 하나하나 던져 가면서 정답을 맞추어 나가는 모델 (like 스무고개)

데이터를 잘 나눌 수 있는 질문들을 찾아서 정확한 값을 도출한다.
"""
None

from sklearn.tree import DecisionTreeClassifier

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

dt.score(train_scaled, train_target), dt.score(test_scaled, test_target)

"""
↑ 로지스틱 회귀에 비해 높은 점수이긴 하나 훈련세트에 대한 점수가 매우 높다 (거의 다 맞춤?)
테스트 세트에 대한 점수가 상대적으로 많이 낮다. => 과대적합(Overfit) 의심.
"""
None

"""
사이킷럿의 결정 트리 알고리즘은 노드에서 최적의 분할을 찾기 전에 특성의 순서를 섞습니다.
따라서 약간의 무작위성이 주입되는데 실행할 때마다 점수가 조금씩 달라질 수 있기 때문입니다.
여기에서는 실습한 결과가 같도록 유지하기 위해 random_state 를 지정합니다.
"""
None

plot_tree() 로 이해하기 쉬운 트리 그림을 시각화 한다.

from sklearn.tree import plot_tree

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

'노드' 는 결정트리를 구성하는 핵심요소

훈련데이터의 특성에 대한 테스트를 표현

ex) 현재 샘플의 당도가 -0.239 보다 작거나 같은지?

테스트 결과 (True, False)에 따라 2개의 가지(branch)를 가집니다.

최상위 노드 : root node

맨 아래 끝의 노드 : leaf node

max_depth로 트리의 깊이를 제한해서 출력해보자

filled : 클래스에 맞게 노드의 색상을 입힌다

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

[노드에 표시된 내용]

1. 테스트 조건

2. 불순도 (gini)

3. 총 샘플수 (samples) : 현 규칙(노드)의 데이터 건수

4. 클래스별 샘플 수 (value) : 클래스 값 기반의 데이터 건수

결정트리에서 예측하는 방법

리프노드에서 가장 많은 클래스가 예측 클래스가 된다. (KNN과 유사)

불순도

'gini' : 지니 불순도 (Gini impurity)

DecisionTreeClassifier 클래스의 criterion 매개변수 기본값이 'gini'

criterion 매개변수 : 노드에서 데이터를 분할 할 기준을 정함.

지니 불순도 계산 방식

클래스의 비율을 제곱해서 더한 다음 1에서 빼면 됨.

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

루트 노드의 지니 불순도를 계산해보면

1 - ((1258 / 5197)² + (3939/5197)²) = 0.367

만약 100 개의 샘플이 있는 어떤 노드의 두 클래스 비율이 정확히 1/2 씩 이라면?

1 - ((50/100)² + (50/100)²) = 0.5 <- 불순도가 '최악'

노드에 하나의 클래스만 있다면

1 - ((100/100)² + (0/100)²) = 0 <- 불순도가 0 (순수노드)

결정트리 모델은 부모노드 (parent node) 와 자식노드(child node)의 불순도 차이가

가능한 커지도록 트리를 성장시켜 나간다.

'부모-자식 노드의 불순도 차이' 계산 방식 => 이를 정보이득(information gain) 이라고 함.

자식 노드의 불순도를 샘플 계수에 비례하여 모두 더한다. 그 다음 부모노드의 불순도에서 뺴면됩니다.

부모의 불순도

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

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

0.367 - (2922 / 5197) x 0.481 - (2257 / 5197) x 0.069 = 0.066

criterion = 'entropy' 를 지정하면 '엔트로피 불순도' 사용

제곱이 아닌 밑이 2인 로그를 사용하여 곱함.

-음성 클래스 비율 x log₂(음성클래스비율) - 양성 클래스 비율 x log₂(양성클래스 비율)

= -(1258 / 5197) x log₂(1258 / 5197) - (3939 / 5197) X log₂(3939 / 5197) = 0.798

불순도 기준으로 '정보이득' 이 최대가 되도록 노드를 분할해 나감.

노드를 순수하게 나눌수록 정보이득은 커집니다.

새로운 샘플에 대해 예측할때에도 노드의 질문에 따라 트리를 이동함.

마지막에 도달한 노드의 클래스 비율을 보고 예측을 만든다.

앞의 트리는 제한없이 자라났기 때문에 훈련세트에 대해서만 정확히 맞추고 테스트 세트의 점수가 낮아졌다

괴대적합 발생 (일반화가 안됨.)

가지치기

max_depth = 3 값 지정. 최대 3개의 노드까지만 depth가 성장 할 수 있다.

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

dt.score(train_scaled, train_target), dt.score(test_scaled, test_target)

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

"""
위에서 레드와인 (음성클래스)로 판정하는 경우는?

당도는 -0.239보다 작고, -0.802보다 커야하고, 알코올 도수는 0.454 보다 작아야한다.
"""
None

  • 결정트리는 전처리 (스케일링) 과정 필요없다!
    • 특성값의 스케일이 계산에 영향을 주지 않는다!

"""
그런데, 당도가 음수다? (말이 안된다.) => 이유는? 전처리(표준화) 했기 때문.

결정트리는 '불순도'를 기준으로 샘플을 나눈다 => 클래스별 비율을 가지고 계산한다.
따라서 스케일은 영향을 주지 않는다!

전처리 필요가 없다 => 결정트리 알고리즘의 장점중 하나
"""
None

전처리 하기전의 train_input, test_input으로 다시 훈련

dt = DecisionTreeClassifier(max_depth=3, random_state=42)
dt.fit(train_input, train_target)

dt.score(train_input, train_target), dt.score(test_input, test_target)

결과는 정확히 같다.

시각화를 해보자

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

"""
이제 제대로 된 수치로 에측 결과에 대한 수치 설명이 가능해졌다.

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

그 외에는 모두 화이트 와인으로 예측
"""
None

특성 중요도 (feature importance)

dt.featureimportances

↑ 역시 두번째 특성인 당도가 가장 중요도가 높았다.


검증세트

validation set

이전예제

train : test = 8 : 2

이번에는

train : validation : test = 6 : 2 : 2

훈련은 => train

모델검증 => validation (매개변수를 바꿔가며 가장 좋은 모델을 선택) 여기서 매개변수는 하이퍼 파라미터임.

가장 좋은 모델을 선택한 후

train + validation 합쳐서 다시 훈련.

마지막으로 최종점수 => test

train_input.shape, test_input.shape # 8:2로 쪼개놓은 상태

위의 train 데이터를 다시 쪼개어 validation 세트를 만들거다.

sub_input, val_input, sub_target, val_target = \
train_test_split(train_input, train_target, test_size = 0.2, random_state=42)

sub_input.shape, val_input.shape

train : validation : test = 4157 : 1040 : 1300

위 입력데이터로 모델을 만들고 평가해보자

dt = DecisionTreeClassifier(random_state=42)
dt.fit(sub_input, sub_target)

print(dt.score(sub_input, sub_target))
print(dt.score(val_input, val_target)) # validation 으로 모델 평가

과대적합 되어있다. 매개변수(하이퍼 파라미터)를 바꿔가면서 더 좋은 모델을 찾아야한다.

우선 검증세트에 대해 더 알아봐야한다.

교차 검증 cross validation

k-fold 교차검증

디폴트 : 5-fold

cross_validate() : 교차 검증 함수

from sklearn.model_selection import cross_validate

scores = cross_validate(dt, train_input, train_target)

scores

fit_time : 모델 훈련하는 시간

score_time : 모델 검증하는 시간

test_time : 검증 점수

디폴트로 5-fold 교차검증 수행 -> 각각 5개의 값이 담겨있다.

k-fold 값은 cv= 로 조정 가능.

np.mean(scores['test_score']) # 교차검증의 최종점수는 5개의 점수를 평균하여 얻을 수 있다.

cross_validate() 는 훈련세트를 섞어 fold를 만들지 않는다.

교차 검증시 훈련세트를 섞으려면 분할기(splitter) 를 지정해야한다.

회귀 모델의 경우 'KFold 분할기' 사용

분류 모델의 경우 'StratifiedKFold' 사용

직전에 수행한 교차검증 코드는 아래와 동일

from sklearn.model_selection import StratifiedKFold
scores = cross_validate(dt, train_input, train_target, cv = StratifiedKFold())

np.mean(scores['test_score'])

10-fold 교차 검증 수행하려면

splitter = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)
scores = cross_validate(dt, train_input, train_target, cv = splitter)

np.mean(scores['test_score'])

scores

하이퍼 파라미터 튜닝

모델이 학습하기 전에 세팅을 해주는것

사람의 개입없이 하이퍼 파라미터 튜닝을 자동으로 수행하는 기술 : AutoML

결정트리에는

mex_depth, min_samples_split ... 등 매개변수들이 있으나

순차적으로 최적값을 찾아내는건 불가능합니다.

위 매개변수들을 동시에 바꿔가며 최적의 값을 찾아내야 합니다.

-> GridSearch 사용!

GridSearchCV 을 사용하면 하이퍼 파라미터 탐색과 교차검증을 한번에 수행

from sklearn.model_selection import GridSearchCV

params = {
'min_impurity_decrease' : [0.0001, 0.0002, 0.0003, 0.0004, 0.00005],
}

gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)

min_impurity_decrease 값을 바꿔가며 5번 실행

각 min_impurity_decrease 값마다 5-fold 교차검증

5 x 5번의 훈련

gs.fit(train_input, train_target)

최적의 파라미터로 학습된 모델

dt = gs.bestestimator
dt

dt.score(train_input, train_target)

최적의 파라미터값

gs.bestparams

5 x 5 훈련 수행한 점수

gs.cvresults

pd.DataFrame(gs.cvresults)

교차검증의 평균점수

gs.cvresults['mean_test_score']

bestindex = np.argmax(gs.cv_results['mean_test_score'])
best_index

gs.cvresults['params'][best_index]

조금 더 복잡한 매개변수 조합에 도전!

params = {
'min_impurity_decrease' : np.arange(0.0001, 0.001, 0.0001), # 9개 값
'max_depth' : range(5,20,1), # 15개 값
'min_samples_split' : range(2, 100, 10), #10개의 값.
}

파라미터 조합 : 9 x 15 x 10 = 1350개

훈련 횟수는 : 1350 x 5-fold = 6750번 학습

gs = GridSearchCV(DecisionTreeClassifier(random_state=42), params, n_jobs=-1)
gs.fit(train_input, train_target)

gs.bestparams

np.max(gs.cvresults['mean_test_score'])

pd.DataFrame(gs.cvresults).shape

매개변수의 값이 '수치' 일때 값의 범위나 간격을 미리 정하기 어려울 수 있다.

또한, 너무 많은 매개변수 조건이 있으며 Grid Search 수행시간이 너무 오래걸릴수도있다.

이럴때 랜덤서치를 사용하면 좋다.

랜덤서치에는 매개변수 값의 목록을 전달하는게 아니라,

매개변수를 샘플링 할 수 있는 '확률 분포' 객체를 전달.

from scipy.stats import uniform, randint

uniform, randint 는 주어진 범위에서 고르게 값을 뽑습니다. -> '균등분포에서 샘플링' 한다.

실수값, 정수값

0 ~ 10 사이 범위를 갖는 randint 객체 만들고 10개 숫자 샘플링

rgen = randint(0,10)
rgen

rgen.rvs(10)

np.unique(rgen.rvs(1000), return_counts = True) # 정말 균등하게 뽑아내나?

ugen = uniform(0, 1) # 확률분포 객체
ugen.rvs(10)

랜덤서치에 위의 randint, uniform 객체를 넘겨주고

총 '몇번의 샘플링' 해서 최적의 매개변수를 찾을지 지정해줄 수 있다.

탐색할 매개변수

params = {
'min_impurity_decrease' : uniform(0.0001, 0.001), # 0.0001에서 0.001 사이의 실수값 샘플링
'max_depth' : randint(20, 50), # 20 에서 50 사이의 정수값 샘플링.
'min_samples_split' : randint(2, 25), # ...
'min_samples_leaf' : randint(1,25), # 리프 노드가 되기위한 최소 샘플의 개수

                                    # 어떤 노드가 분할하여 만들어질 자식노드의 샘플수가 이 값보다 작을 경우 분할하지 않는다.

}

from sklearn.model_selection import RandomizedSearchCV

gs = RandomizedSearchCV(DecisionTreeClassifier(random_state=42), params,
n_iter = 100, # 샘플링 횟수
n_jobs = -1, random_state=42
)

gs.fit(train_input, train_target)

gs.bestparams

np.max(gs.cvresults['mean_test_score'])

dt = gs.bestestimator # 최적 파라미터로 훈련된 모델

dt.score(test_input, test_target)

profile
독해지자

0개의 댓글