머신러닝&딥러닝/책요약및리뷰

[혼자공부하는머신러닝+딥러닝] 5. 트리 알고리즘

e냥냥 2022. 6. 22. 15:08
728x90

아래 내용은 "혼자 공부하는 머신러닝+딥러닝" 을 공부하며 간략하게 정리한 내용입니다. 쉬운 사례로 어려운 수식 없이 설명하므로 입문자에게 강추하는 책입니다!

 


1. 시작하기 전에

한빛 마켓에서는 이번에 신상품으로 캔 와인을 판매하기로 했는데 레드와인과 화이트와인 표시가 누락되어 김팀장이 담당자를 급하게 소환합니다!! 알코올 도수, 당도, PH값으로 와인 종류를 구분할 수 있는 방법이 있는지 물어보는데요 .....

 

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

먼저 품질관리팀에서 보내온 6,497개의 와인 샘플 데이터로 기존에 사용하던 로지스틱 회귀 모델을 사용하여 분류해보고자 합니다. 

import pandas as pd

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

# 처음 5개 샘플 확인
display(wine.head())

# 각 열의 데이터 타입과 누락값 확인
print(wine.info())

# 각 열의 간략한 통계
print(wine.describe())

[그림1] 와인 데이터 요약

 

처음 3개의 열(alcohol, sugar, PH)는 각각 알코올 도수, 당도, PH값을 나타냅니다. 네 번째 열은 타깃값으로 0이면 레드와인, 1이면 화이트와인이라고 합니다. 레드/화이트 와인을 구분하는 이진 분류 문제이고 화이트와인(1)을 골라내는 문제입니다. info() 메서드를 통해 각 열의 데이터 타입과 결측값을 확인할 수 있는데 총 6,497개 샘플이 있고 모든 열은 실수값이며 결측값이 없는 것을 알 수 있습니다. 

 

[그림2] 와인 데이터 통계량

각 열에 대한 간략한 통계를 출력해주는 describe() 메서드를 살펴보면 최소(min), 최대(max), 평균(mean), 표준편차(std),  1사분위수(25%), 중간값(50%), 3사분위수(75%) 값을 알려주고 있습니다. 

 

기본적인 로지스틱 모델을 학습해보겠습니다. 

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

print(lr.score(test_scaled,  test_target))
→ 0.7776923076923077

 

학습/테스트셋 분할시 train_test_split() 함수에 설정값을 지정하지 않으면 default값으로 25%로 지정합니다. 샘플 개수가 충분히 많기 때문에 20%정도만 테스트셋으로 나누었습니다. (test_size = 0.2)

 

학습셋 정확도 0.78, 테스트셋 정확도 0.77로 점수가 그리 높진 않지만 보고를 위해 보고서를 작성합니다. 해당 모델을 설명하기 위해 로지스틱 회귀 모델이 학습 계수와 절편을 출력해서 보고서를 작성하였는데 이해하기 어렵다며 순서도처럼 쉽게 설명해서 다시 가져오라고 합니다....

 

3. 결정트리

담당자는 선배에게 '설명하기 쉬운' 모델로 결정트리(Decision Tree) 모델을 추천받았습니다. 사이킷런 tree 라이브러리에서 DecisionTreeClassifier 클래스를 사용하여 모델을 학습하고 평가해보겠습니다. 

# 결정트리
from sklearn.tree import DecisionTreeClassifier

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

print(dt.score(train_scaled, train_target))
→ 0.996921300750433

print(dt.score(test_scaled, test_target))
→ 0.8592307692307692

 

로지스틱 회귀보다 훈련셋과 테스트셋의 평가 점수가 높아졌습니다! 모델을 plot_tree() 함수를 이용하여 그림으로 표현해보도록 하겠습니다. 

# 결정트리 시각화
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

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

[그림3] 와인데이터 결정트리 그림

 

나무(tree) 모양과는 반대로 위에서부터 아래로 거꾸로 자라는 모습의 트리 그림입니다. 맨 위의 노드(node)루트 노드(root 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()

[그림4] 와인데이터 결정트리, 깊이 :1

 

그림을 해석해보겠습니다. 

첫 번째 루트노드는 당도(sugar)가 -0.239 이하인지 질문합니다. 어떤 샘플의 당도가 -0.239와 같거나 작으면 왼쪽 가지로 가고 그렇지 않으면 오른쪽 가지로 이동합니다. 즉 왼쪽이 Yes, 오른쪽이 No입니다. 루트 노드의 총 샘플 수는 5,197개로 음성클래스(레드와인) 1,258개, 양성클래스(화이트와인)은 3,939개입니다. 

 

두번째 줄에서 왼쪽 노드를 해석해보겠습니다. 

당도가 -0.802와 같거나 낮다면 다시 왼쪽으로 그렇지 않으면 오른쪽 가지로 이동합니다. 이 노드에서 음성 클래스와 양성클래스의 샘플 개수는 각각 1,177개와 1,745개입니다. 루트 노드보다 양성 클래스(화이트와인)의 비율이 크게 줄었습니다. 이유는 오른쪽 노드를 보면 알 수 있습니다. 

 

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

 

4. 불순도(gini)

gini는 지니 불순도(gini impurity)를 의미합니다. DecisionTreeClassifier 클래스의 criterion 매개변수의 기본값이 'gini'입니다. criterion  매개변수의 용도는 노드에서 데이터를 분할하는 기준을 정하는 것입니다. 지니 불순도를 계산하는 방법에 대해 알아보겠습니다. 

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

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

 

다중 클래스 문제라면 클래스가 더 많겠지만 계산하는 방법은 동일합니다. 그럼 이전 트리 그림에 있던 루트 노드의 지니 불순도를 계산해봅시다. 루트 노드는 총 5,197개의 샘플이 있고 그중에 1,258개가 음성클래스, 3,939개가 양성클래스입니다. 따라서 다음과 같이 지니 불순도를 계산할 수 있습니다. 

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

 

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

불순도 차이를 계산하는 방법을 알아보겠습니다. 먼저 자식 노드의 불순도를 샘플 개수에 비례하여 모두 더하고 부모 노드의 불순도에서 빼면 됩니다. 

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

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

 

이런 부모와 자식 노드 사이의 불순도 차이를 정보이득(information gain) 이라고 부릅니다. 결정 트리는 정보이득이 최대가 되도록 데이터를 나누는데 사이킷런에는 또 다른 불순도 기준이 있습니다.

 

DecisionTreeClassifier 클래스에서 criterion = 'entropy'를 지정하여 엔트로피 불순도를 사용할 수  있습니다. 

루트 노드의 엔트로피 불순도는 다음과 같이 계산할 수 있습니다. 

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

$$ = -(1258/5197) × log_2(1258/5197) - (3939/5197) × log_2(3939/5197) = 0.798$$

 

보통 기본값인 지니 불순도와 엔트로피 불순도가 만든 결과의 차이는 크지 않기 때문에 책에서는 기본값인 지니 불순도를 사용하였습니다.

 

5. 가지치기

과수원에서 열매를 잘 맺기 위해 가지치기를 하는 것처럼 결정 트리도 가지치기를 해야합니다. 그렇지 않으면 끝까지 자라나는 트리가 만들어지는데 훈련셋에는 잘 맞지만 테스트셋에서는 잘 맞지 않을 것 입니다. 이를 두고 일반화가 잘 안될 것 같다고 말합니다. 

 

결정 트리에서 가지치기를 하는 가장 간단한 방법은 자라날 수 있는 트리의 최대 깊이를 지정하는 것입니다. DecisionTreeClassifier 클래스의 max_depth 매개변수를 3으로 지정하여 모델을 만들어보겠습니다.

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

print(dt.score(train_scaled, train_target))
→ 0.8454877814123533

print(dt.score(test_scaled, test_target))
→ 0.8415384615384616

 

훈련셋의 성능은 낮아졌지만 테스트셋의 성능은 거의 그대로 입니다. plot_tree() 함수로 그려보겠습니다. 

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

[그림5] 와인데이터 결정트리, 깊이 :3

 

노드를 나누는 기준을 보면 특성값을 어떤 음수값을 기준으로 나누고 있는 노드가 보입니다. 어떻게 해석해야할까요?

잎서 불순도를 기준으로 샘플을 나누고 불순도는 클래스별 비율을 가지고 계산하기 때문에 특성값의 스케일에 영향을 미치지 않습니다. 따라서 표준화 전처리를 할 필요가 없는 결정 트리 알고리즘의 장점 중 하나입니다. 

 

전처리 하기 전의 데이터로 다시 학습하고 트리를 그려보겠습니다. 

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

print(dt.score(train_input, train_target))
→ 0.8454877814123533

print(dt.score(test_input, test_target))
→ 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)가 가장 유용한 특성 중 하나일 것 같습니다.

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

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

 

두번째 특성인 당도가 0.87정도로 특성 중요도가 가장 높습니다. 그 다음 알코올 도수, pH 순이네요. 이 값을 모두 더하면 1이 되고 특성 중요도는 각 노드의 정보 이득과 전체 샘플에 대한 비율을 곱한 후 특성별로 더하여 계산됩니다. 

 

아주 좋은 성능을 보이진 않지만 보고하기에 괜찮은 모델입니다. 

결정 트리는 많은 앙상블 학습 알고리즘의 기반이 되는데 다음장에서는 다양한 매개변수, 즉 하이퍼파라미터를 자동으로 찾기 위한 방법을 알아보고 그 다음 앙상블 학습을 다루어 보겠습니다. (다음장에서 계속...)

 

 

감사합니다 :)

 

 

참고자료

혼자 공부하는 머신러닝+딥러닝

 

 

 

728x90
loading