ML | Decision Tree, Cross validation, Ensemble



decision tree, cross validation, ensemble에 대해 정리하는 글입니다.



Decision Tree

Decision tree는 마치 순서도 처럼 예/아니오 같은 이진으로 분류된 기준을 이어나가면서 분류를 하는 알고리즘 입니다. 회귀도 가능하지만 분류 위주로 정리되었습니다. Decision Tree의 장점은 해석하기 쉽다는데 있습니다.


와인 분류하기

Decision Tree의 장점을 확인하기 위해 Logistic Regression의 분류와 비교해보겠습니다.

아래의 데이터로 실습을 해보겠습니다. 와인의 알콜 농도, 당도, pH와 와인의 클래스입니다.

클래스가 0 이면 레드와인, 1이면 화이트 와인입니다.

와인 클래스를 예측하는 모델을 Logistic RegressionDecision Tree로 각각 진행해보겠습니다.

1
2
3
import pandas as pd
wine = pd.read_csv("https://bit.ly/wine_csv_data")
wine
Index alcohol sugar pH class
0 9.4 1.9 3.51 0
1 9.8 2.6 3.2 0
2 9.8 2.3 3.26 0
3 9.8 1.9 3.16 0
4 9.4 1.9 3.51 0
6492 11.2 1.6 3.27 1
6493 9.6 8 3.15 1
6494 9.4 1.2 2.99 1
6495 12.8 1.1 3.34 1
6496 11.8 0.8 3.26 1

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

train과 test를 나누고 Standardization을 해줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data = wine[["alcohol", "sugar", "pH"]]
target = wine["class"]

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

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

ss = StandardScaler()
ss.fit(train_input)

train_scaled = ss.transform(train_input)
test_scaled = ss.transform(test_input)

LogisticRegression으로 분류하여 성능을 확인합니다.

1
2
3
4
5
6
7
8
9
10
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

train셋은 0.78, test셋은 0.77의 정확도를 보입니다. 성능은 좋아보이지 않습니다. 선형 방정식이 어떻게 학습되었는지 확인해보겠습니다.


1
2
3
4
5
print(dict(zip(train_input.columns, lr.coef_[0])))
print(lr.intercept_)

# {'alcohol': 0.512702742045543, 'sugar': 1.6733910972911463, 'pH': -0.6876778082262984}
# [1.81777902]

데이터에 대한 weight와 intercept는 다음처럼 나타났습니다. 이를 해석하기는 어렵습니다. 우리는 이 수치를 보고는 알코올과 당도가 높을 수록 화이트 와인일 가능성이 높고, pH가 높을 수록 레드와인일 가능성이 높다고만 생각할 순 있지만 수치가 어떤 의미가 있는지는 알 수 없습니다. 여기서 polynomial로 만든다면 더더욱 알기 어렵습니다.



Decision Tree으로 분류를 해보겠습니다.

1
2
3
4
5
6
7
8
9
10
from sklearn.tree import DecisionTreeClassifier

dt_clf = DecisionTreeClassifier()
dt_clf.fit(train_scaled, train_target)

print(dt_clf.score(train_scaled, train_target))
print(dt_clf.score(test_scaled, test_target))

# 0.996921300750433
# 0.86

train셋은 0.99, test셋은 0.86의 정확도를 보여줍니다. 점수가 train이 훨씬 높으므로 overfitting이 있는거 같습니다.

decision Tree가 어떻게 분류했는지 확인해보죠.

시각화를 위해 matplotlib와 scikit-learn의 plot_tree를 이용합니다.

1
2
3
4
5
6
import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

plt.figure(figsize= (10, 7))
plot_tree(dt_clf, rounded=True, filled = True, feature_names = ["alcohol", "suger", "pH"])
plt.show()

복잡해 보이는 그림이 나왔습니다. decision tree가 결정하는 방식을 보여줍니다. rooted binary tree 구조입니다. 근데 보이지 않으니 max_depth를 설정해서 깊이 1까지만 확인해 보겠습니다.

1
2
3
plt.figure(figsize= (10, 7))
plot_tree(dt_clf, max_depth = 1, rounded=True, filled = True, feature_names = ["alcohol", "suger", "pH"])
plt.show()

상자 안을 설명하면 다음과 같습니다.

  • 테스트 조건 - sugar
  • 불순도 (impurity) - gini
  • 총 샘플 수 - 5197
  • 클래스 별 샘플 수 - [1258, 3939]

루트 노드를 예로 들면, 당도가 -0.239 이하가 맞으면 왼쪽 노드로 이동하고, 아니라면 오른쪽 노드를 순회하여 내려갑니다.

각각의 노드에서 한 쪽 클래스를 잘 예측할 수록 색깔을 진하게 표시합니다.

Impurity

scikit-learn의 DecisionTreeClassifier는 impurity를 측정하여 이를 기준으로 나눕니다.

criterion 파라미터를 보면 다음과 같습니다.

criterion{“gini”, “entropy”, “log_loss”}, default=”gini”
The function to measure the quality of a split. Supported criteria are “gini” for the Gini impurity and “log_loss” and “entropy” both for the Shannon information gain, see Mathematical formulation.

수식을 처음부터 살펴 보면 …

  • 하나의 숫자로 이루어진 실수를 스칼라라고 부르며, $ x $같이 변수로 나타낼 수 있습니다. 실수 집합인 $ R $ 의 원소이고 이를 표시하면 아래와 같습니다.
\begin{align} x \in R \end{align}
  • 여러 숫자가 특정 순서대로 모여있으면 벡터라고 합니다.
    train셋을 x로, test셋을 y로 했을때 벡터를 다음처럼 나타낼 수 있습니다.
\begin{cases} \text{train vector}=x_i \in R^n &(i=1, ..., l) \\\ \text{target vector}=y \in R^l \end{cases}
  • 주어진 train, target 벡터에 대해 decision tree는 같은 target class를 가진 데이터나 비슷한 target class끼리 sub 그룹을 만듭니다.

  • 노드 $ m $에 대해 분류에 사용할 $ n_m $ 개의 샘플의 집합을 $ Q_m $으로 표현합니다.

  • feature $ j $에 대한 threshold $ t_m $ 으로 분류할 candidate split은 $ \theta = (j, t_m) $ 으로 나타냅니다.

  • 분류될 샘플들은 $ Q_m^{left}(\theta)$ 와 $ Q_m^{right}(\theta) $ 으로 subset이 되어 나누어집니다.

\begin{cases} Q_m^{left}(\theta) = {(x, y)|x_j \le t_m}\\\ Q_m^{right}(\theta) = Q_m \setminus Q_m^{left}(\theta) \end{cases}

$ A \setminus B $ 는 차집합set difference으로 $ B $ 의 원소가 포함되지 않는 $ A $ 의 원소를 말합니다.

잠깐 정리를 하면, 노드 안에서는 분류할 데이터 집합을 Q로 나타내고, feature의 분류 기준 (threshold)에 따라 $ Q^{left} $, $ Q^{right} $ 로 나누어 집니다.

이제 여기서, node $ m $의 candidate split에 대한 quality를 함수 $ H() $ 로 계산합니다. 함수의 종류는 해결하고자 하는 모델 종류에 따라 달라집니다. 보통은 gini 와 entropy를 사용합니다.

클래스 K개가 0, 1, 2, … K-1 이고, 노드 $ m $ 에 대한 클래스의 비율을 나타내면 다음과 같습니다.

\begin{align} p_{mk} = \frac {1} {n_m} \sum_{y\in{Q_m}}I(y=k) \end{align}

gini

\begin{align} H(Q_m) = 1 - \sum_{m} (p_{mk})^2 \end{align}
  • 쉽게 말하면 클래스의 비율을 제곱해서 모두 더하고 1을 빼면 됩니다.
  • 이 값은 0~0.5 사이로 나타나며, 0에 가까울 수록 분류가 잘되고, 0.5에 가까울 수록 분류가 되지 않습니다.
  • 0은 (completely) pure node라고 합니다.

entropy

\begin{align} H(Q_m) = -\sum_{m}p_{mk}\log_{2}(p_{mk}) \end{align}
  • entropy도 클래스의 비율을 사용하지만 log2를 이용합니다.
  • 이 값은 0~1 사이로 나타나며, 0에 가까울 수록 분류가 잘되고, 1에 가까울 수록 분류가 되지 않습니다.
  • 0은 (completely) pure node라고 합니다.

다시 정리하면, loss function $ H $ 를 이용해 클래스의 비율에 따라 잘 분류했는지에 대한 정보인 impurity를 계산합니다.

이제 impurity를 child 노드에 대해 계산합니다.

\begin{align} G(Q_m, \theta) = \frac {n_m^{left}} {n_m} H(Q_m^{left}(\theta)) + \frac {n_m^{right}} {n_m} H(Q_m^{right}(\theta)) \end{align}

parent 노드와의 impurity 차이를 계산하여 구하고, 이를 information gain 이라고 합니다. impurity를 최소화 하는 파라미터를 찾아서 선택합니다.

\begin{align} \theta^* = {\arg}{\min}_\theta G(Q_m, \theta) \end{align}

다시 그림으로 돌아와서…

sklearn에서는 기본으로 gini를 사용해 impurity를 계산했습니다.

root node를 기준으로

  • 전체 샘플 수는 5197 이고, subset은 1258, 3939개로 나누어집니다.
  • gini를 계산하면 다음과 같습니다.
\begin{align} 1 - ((\frac {1258} {5197})^2 +(\frac {3939} {5197})^2) = 0.397 \end{align}

Pruning

decision tree가 child node를 계속 학습해서 무한히 내려가면 어떻게 될까요? 끝까지 가면 train셋에는 분명 좋은 성능을 보일 것이지만, overfitting이 될 가능성이 높습니다. 첫 번째 그림이 바로 그 결과입니다.

이는 decision tree의 단점이고, 적절하게 가지치기 (pruning)을 해야합니다.

DecisionTreeClassfier로 처음 모델을 만들 때, max_depth를 이용해 설정할 수 있습니다. 3으로 지정하여 실습해보겠습니다.

1
2
3
4
5
6
7
8
dt_clf = DecisionTreeClassifier(max_depth=3)
dt_clf.fit(train_scaled, train_target)

print(dt_clf.score(train_scaled, train_target))
print(dt_clf.score(test_scaled, test_target))

# 0.8454877814123533
# 0.8415384615384616

train셋에 대해서 점수는 떨어졌지만 test셋은 그대로이고 두 데이터셋의 차이가 없어보입니다. 이제 tree를 그려보면 다음과 같습니다.

max_depth가 설정한대로 3까지만 존재합니다.
하지만 아직 설명하기 어려운 부붕니 있습니다. root node를 기준으로 보면, 표준화된 당도의 값이 -0.239보다 낮고, … 라고 말하는 것은 해석에 어려움이 있습니다. decision tree의 장점 중 하나는 표준화 전처리 여부는 decision tree에 영향을 주지 않는 것입니다. impurity는 나누어진 클래스의 비율만을 가지고 계산하기 때문이죠.

표준화 전처리 하지 않은 데이터로 다시 score를 계산하고 plotting을 해보겠습니다.

1
2
3
4
5
6
7
8
dt_clf = DecisionTreeClassifier(max_depth=3)
dt_clf.fit(train_input, train_target)

print(dt_clf.score(train_input, train_target))
print(dt_clf.score(test_input, test_target))

# 0.8454877814123533
# 0.8415384615384616

스코어가 그대로 인것을 확인할 수 있습니다.

impurity 값들도 그대로입니다. 가장 좋은것은 당도, 알콜농도, pH를 있는 그대로의 기준으로 설명할 수 있습니다.

당도가 1.625보다 크고 4.324보다 작은 와인에서 도수가 11.025 이하라면 레드와인, 아니라면 화이트 와인으로 분류합니다.

그리고 각각의 특성에 대한 중요도를 feature_importances_ 메써드로 확인할 수 있습니다.

1
2
3
4
5
dict(zip(train_input.columns, dt_clf.feature_importances_))

# {'alcohol': 0.12345625703073809,
# 'sugar': 0.8686293409940407,
# 'pH': 0.007914401975221242}

결과는, 당도가 와인을 잘 구분할 수 있는 기준이 된다는 것을 알려줍니다. 이렇게 feature 별로 중요도를 통해 feature selection에 활용할 수 있습니다.


교차검증과 그리스 서치

지금까지 데이터셋을 훈련세트과 테스트세트로 나누고 훈련세트를 통해 학습시킨 모델로 테스트세트를 평가했습니다.

그런데 테스트세트에 대한 성능을 계속 확인하면서 모델을 만든다면, 점점 테스트세트에 맞춘 모델이 됩니다.

머신러닝이 아닌 직접 만든 알고리즘의 경우, 이런 사례가 많습니다…😮😮

그러므로, 만들어진 모델에 대해서 테스트세트의 성능으로 앞으로의 성능도 이럴 것이다라는 일반화를 신뢰하려면, 테스트세트는 가능한한 사용하지 않고, 마지막 검증에만 사용하는것이 좋습니다.

하지만, 테스트세트를 사용하지 않는다면 overfitting, underfitting을 확인할 수 없습니다.

이 경우 검증 세트validation set가 해결해 줄수 있습니다.




Train, Validation, Test set



아주 간단하게 훈련세트에서 또다시 일부를 나눈 것입니다. 보통 20~30%를 테스트세트와 검증세트로 나눕니다.

위에서 사용한 데이터를 그대로 사용해보겠습니다.


검증세트 만들기

1
2
3
4
5
sub_input, val_input, sub_target, val_target = train_test_split(
train_input, train_target, test_size = 0.2, random_state=42)

print(sub_input.shape, val_input.shape)
# (4157, 3) (1040, 3)

train_test_split에 train_input을 넣었습니다


훈련세트와 검증세트로 score확인

1
2
3
4
5
6
7
8
dt_clf = DecisionTreeClassifier()
dt_clf.fit(sub_input, sub_target)

print(dt_clf.score(sub_input, sub_target))
print(dt_clf.score(val_input, val_target))

# 0.9971133028626413
# 0.8586538461538461

점수를 보니 훈련세트에 overfitting되어 있음을 확인할 수 있었습니다.


교차 검증

검증세트가 왜 사용하는지는 이해가 됩니다만, 이러면 훈련세트의 크기가 줄어들어서 학습이 제대로 되지 않을 것 같습니다. 그렇다고 검증세트의 크기를 줄이면 제대로 검증이 되지 않을 것입니다.

그래서 검증세트는 기본적으로 교차검증Cross validation을 이용합니다.




교차 검증의 예시



교차검증이란, 검증세트를 나누어 평가하는 과정을 여러번 반복합니다. 보통 K번 반복하는 교차검증을 K-fold cross validation이라고 합니다.

위 그림에서 검정색 네모칸을 검증세트에 대해서 K번 평가를 합니다. 그리고 보통 평가된 점수의 평균을 대표값으로 사용합니다.

scikit-learn에서는 이를 자동으로 해주는 cross_validate라는 함수를 제공합니다.

1
2
3
4
5
6
7
8
9
from sklearn.model_selection import cross_validate

dt_clf = DecisionTreeClassifier()
scores = cross_validate(dt_clf, train_input, train_target)
print(scores)

# {'fit_time': array([0.00964594, 0.00899792, 0.00821495, 0.00859427, 0.00850391]),
# 'score_time': array([0.002074 , 0.0016861 , 0.00194216, 0.00178385, 0.00165391]),
# 'test_score': array([0.87115385, 0.85576923, 0.87776708, 0.85466795, 0.8373436 ])}

fit_time, score_time, test_score를 가진 dictionary를 반환합니다. cross_validate는 5-fold가 default이므로 값도 5개씩 있습니다. cv 매개변수로 Fold 수를 바꿀 수 있습니다.

  • fit_time: 훈련하는 시간
  • score_time: 검증하는 시간
  • test_score: 검증 점수

검증 점수의 평균은 다음과 같습니다.

1
2
print(scores["test_score"].mean())
# 0.8545302435774044

여기서는 train_input과 train_target에 대해 cross_validation을 했으므로, test_score의 점수는 이름은 test지만 검증세트라고 보면 됩니다.

Splitter

지금까지는 train_test_split으로 전체 데이터를 섞고 훈련세트를 뽑아서 섞을 필요는 없었지만, 교차 검증을 할 때, 훈련 세트를 섞으려면 splitter를 지정해야 합니다.

  • Regression: KFold
  • Classification: StratifiedKFold

지금은 분류이므로 StratifiedKFold를 사용해보겠습니다.

1
2
3
4
from sklearn.model_selection import StratifiedKFold
scores = cross_validate(dt_clf, train_input, train_target, cv = StratifiedKFold())
print(scores["test_score"].mean())
# 0.8601106833493745

cross_validate 함수의 cv 매개변수로 StratifiedKFold()를 넣어주면 됩니다.

기본은 5 fold이고, fold수를 바꾸려면 StratifiedKFold의 n_splits를 바꾸어주면 됩니다.


1
2
3
4
splitter = StratifiedKFold(n_splits = 10, shuffle=True, random_state=42)
scores = cross_validate(dt_clf, train_input, train_target, cv = splitter)
print(scores["test_score"].mean())
# 0.8604972580406105



좋습니다. 교차검증을 진행하면서도 한층 간결한 코드를 사용할 수 있게 되었습니다.

그런데 문제가 하나 있습니다. max_depth같은 사용자가 지정하는 옵션은 어떻게 최적값을 찾을까요?



하이퍼파라미터 튜닝

decision tree에서 max_depth 같은 유저가 직접 지정하는 파라미터를 하이퍼파라미터 라고 합니다.

중요한 점은 모델마다 여러 종류의 하이퍼파라미터를 가질 수 있다는 점이며, 하나의 하이퍼파라미터를 최적화 했다고 고정값이 될수 없다는 것입니다. 예를 들어 decision tree의 max_depth의 최적값을 찾은 다음에 다시 min_samples_split의 최적값을 찾는 것은 의미가 없습니다. 두 파라미터에 대해서 동시에 최적값을 찾아야 합니다.

이를 GridSearchCV 클래스가 편리하게 해결해줍니다.

1
2
3
4
5
6
7
8
9
10
from sklearn.model_selection import GridSearchCV

params = dict(min_impurity_decrease = np.arange(0.0001, 0.001, 0.0001),
max_depth = range(5, 20, 1),
min_samples_split = range(2, 100, 10))

gs = GridSearchCV(DecisionTreeClassifier(random_state = 42),
param_grid = params,
n_jobs = 4)
gs.fit(train_input, train_target)
  • 하이퍼파라미터는 dictionary 형태로 GridSearchCV에는 param_grid에 넣어줍니다. (positional input으로 2번째에 key 없이 넣어줘도 됩니다.)
  • n_jobs는 사용할 병렬 컴퓨팅에 사용할 CPU수를 지정할 수 있습니다. default는 1이고, -1은 가용한 자원을 모두 사용합니다.
    params에 3가지 하이퍼파라미터를 동시에 교차검증을 하므로, 학습은 $ 10x15x10 = 1350 $ 번 하게 되고, 5-fold가 기본이므로 6750개의 모델을 생성합니다. 따라서 하이퍼파라미터를 튜닝할 때에는 사용할 CPU수를 적절히 지정해줘야합니다.

가장 잘찾은 파라미터 조합은 best_params_ 으로 찾을 수 있습니다.

1
2
print(gs.best_params_)
# {'max_depth': 14, 'min_impurity_decrease': 0.0004, 'min_samples_split': 12}

가장 좋은 교차검증 점수는 cv_results_["mean_test_score"]의 최대값으로 볼 수 있습니다.

1
2
print(np.max(gs.cv_results_["mean_test_score"]))
# 0.8683865773302731

가장 좋은 점수의 모델을 best_estimator_으로 저장되어 있고 바로 decision tree 모델처럼 사용할 수 있습니다.

1
2
3
4
5
6
dt_clf = gs.best_estimator_

print(dt_clf.score(train_input, train_target))
# 0.892053107562055
print(dt_clf.score(test_input, test_target))
# 0.8615384615384616



랜덤 서치

하이퍼파라미터를 수치로 입력해야 할 때, 적절한 값의 범위를 알기 어려울 수 있습니다.
예를 들어, min_impurity_decrease 의 기본값은 0.0001 입니다. 대충 0.001 정도를 최대로 해본다고 해도, 0.0001 단위로 할지 0.0002 단위로 할지는 애매합니다.

게다가 너무 많은 값을 그리드서치에 사용하면 수행 시간이 오래 걸릴 수 있습니다.

이럴 때, 랜덤 서치를 사용하면 됩니다.

파라미터 딕셔너리에서 각 매개변수의 value에 랜덤 샘플링을 해주는 함수를 넣어주면 되고 GridSearchCV가 아닌 RandomizedSearchCV를 사용하면 됩니다. scipy의 uniform과 randint를 사용하면 됩니다.

1
2
3
4
5
6
7
8
9
from scipy.stats import uniform, randint

rgen = randint(0, 10)
print(rgen.rvs(10))
# [6 3 6 2 5 9 7 8 8 7]

ugen = uniform(0, 1)
print(ugen.rvs(10))
# [0.74803462 0.10100137 0.88553271 0.1432955 0.1551121 0.16308304 0.44705319 0.25044722 0.66360482 0.10661267]

너무 작은 값이지만 랜덤하게 잘 뽑아 줍니다.



랜덤하게 뽑은 10000개 정수의 분포

1
2
int_array = randint(0, 10).rvs(10000)
pd.DataFrame(int_array).plot.hist()


랜덤하게 뽑은 10000개 실수의 분포

1
2
float_array = uniform(0, 1).rvs(10000)
pd.DataFrame(float_array).plot.hist()


RandomizedSearchCV 사용해보기

아래 params의 각 value는 uniform과 randint로 설정했습니다. 그리고 랜덤으로 뽑아서 수행하기 때문에, RandomizedSearchCV의 n_iter를 설정하여 몇 번 수행할지 정해줘야 합니다. 기본은 10번이고, 여기서는 100으로 해보겠습니다.

GridSearchCV와 달리 param_grid는 positional input으로 넣어줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
from sklearn.model_selection import RandomizedSearchCV

params = dict(min_impurity_decrease = uniform(0.0001, 0.001),
max_depth = randint(5, 20),
min_samples_split = randint(2, 25),
min_samples_leaf = randint(1, 25))

gs = RandomizedSearchCV(DecisionTreeClassifier(random_state = 42),
params,
n_iter = 100,
n_jobs = 4)
gs.fit(train_input, train_target)

최적 파라미터 값의 조합을 출력해보겠습니다.

1
2
print(gs.best_params_)
# {'max_depth': 39, 'min_impurity_decrease': 0.00034102546602601173, 'min_samples_leaf': 7, 'min_samples_split': 13}

가장 높은 교차검증 점수도 확인해보겠습니다.

1
2
print(np.max(gs.cv_results_["mean_test_score"]))
# 0.8695428296438884

최적 모델을 가져오고 테스트셋의 성능을 확인해봅니다.

1
2
3
dt = gs.best_estimator_
print(dt.score(test_input, test_target))
# 0.86

이제, 여러 파라미터로 모델을 만들때, 충분히 다양하게 시도해 볼수 있다고 말할 수 있습니다.

게다가 한정된 자원에서 랜덤하게 파라미터값을 조사해 효율적으로 최적 파라미터도 찾을 수 있게 되었습니다.



트리의 앙상블

이번에는 decision tree를 확장해서 머신러닝에서 아주아주 많이 사용되는 앙상블 모델을 보겠습니다.


랜덤 포레스트

랜덤 포레스트는 decision tree를 랜덤하게 여러개를 만들의 을 만듭니다. 그리고 각 Tree의 예측을 사용해 최종 예측을 만드는 알고리즘입니다.

부트스트랩Bootstrap 샘플이란 데이터 집합에서 복원추출한 샘플입니다. 이를 여러번 복원 추출 하는 과정을 리샘플링Resampling이라고 합니다. 예를 들어 1000개의 샘플에서 1개를 뽑고 다시 돌려놓는 것이 복원 추출인데, 이를 100번 반복해 얻은 샘플입니다. 따라서 중복된 샘플도 허용됩니다. 부트스트랩 샘플은 단지 이렇게 뽑은 결과가 원래 표본과 비슷하다고 생각하고 진행합니다.

랜덤 포레스트는 부트스트랩 샘플을 이용해 각각의 Tree를 학습시킵니다.

scikit-learn에서는 분류와 회귀에 대해 아래 모델이 있습니다.


  • RandomForestClassifer

    • 일부를 무작위로 골라서 이 중에서 최적을 찾아서 노드를 분할합니다. 전체 feature 개수의 제곱근 만큼의 특성을 선택합니다. 즉, 4개의 feature가 있을 떄 2개만 사용합니다.
    • 각 Tree의 클래스별 확률의 평균을 대표값으로 가장 높은 확률을 가진 클래스를 예측값으로 사용합니다.
  • RandomForestRegressor

    • 전체 feature를 모두 사용합니다.
    • 각 트리의 예측값의 평균을 대표값으로 사용합니다.

랜덤포레스트는 decision tree의 하이퍼파라미터를 모두 제공합니다. 그리고 훈련데이터의 특성 중요도도 제공합니다.

1
2
3
4
5
rf.fit(train_input, train_target)
dict(zip(train_input.columns, rf.feature_importances_))
# {'alcohol': 0.23167441093509658,
# 'sugar': 0.5003984054070982,
# 'pH': 0.26792718365780516}

decision tree 실습에서는 당도가 0.86 의 중요도였는데 여기서는 0.5로 낮아졌습니다. 이는 랜덤포레스트가 특성의 일부를 랜덤하게 선택하여 훈려하기 때문입니다. 하나의 특성에 집중하지 않도록 하고 다른 특성도 학습할 기회를 얻습니다. 결론적으로는 overfitting을 줄이고 일반화 성능을 높이게 됩니다.

추가로, 부트스트랩은 복원추출을 하므로 부트스트랩에 포함되지 않는 샘플이 존재합니다. 이를 OOBOut of bag 샘플이라 합니다. 부트스트랩에 포함되지 않았으니 학습에 포함되지 않았겠군요.
랜덤 포레스트는 OOB 샘플을 검증세트처럼 사용해 평가할 수 있습니다.

1
2
3
4
rf = RandomForestClassifier(oob_score = True, n_jobs = 4, random_state = 42)
rf.fit(train_input, train_target)
print(rf.oob_score_)
# 0.8934000384837406



엑스트라 트리Extra Tree

랜덤 포레스트와 비슷하지만, 부트스트랩 샘플을 사용하지 않고 트리를 만들 때 모든 샘플을 사용합니다. 또한 노드를 분할할 때, 가장 좋은 분할이 아닌 무작위로 분할합니다.

무작위이므로 성능은 낮아질 수 있으나, overfitting을 막을 수 있습니다.

1
2
3
4
5
6
7
from sklearn.ensemble import ExtraTreesClassifier

et = ExtraTreesClassifier(n_jobs=4, random_state=42)
scores = cross_validate(et, train_input, train_target,
return_train_score=True, n_jobs=4)
print(scores["train_score"].mean(), scores["test_score"].mean())
# 0.9974503966084433 0.8887848893166506



feature importance도 동일하게 얻을 수 있습니다.

1
2
3
et.fit(train_input, train_target)
print(et.feature_importances_)
# [0.20183568 0.52242907 0.27573525]



그레이디언트 부스팅Gradient boosting

depth가 낮은 tree로 binary tree의 오차를 보완하는 방법입니다.

GradientBoostingClassifier는 default로 depth가 3인 tree를 100개 사용합니다. depth가 3이므로 overfitting에 강하고 높은 일반화 성능을 보입니다.

이전에 정리한 Gradient descent를 사용하여 tree를 앙상블에 추가합니다. 손실 함수는 아래를 사용합니다.

  • classification: logistic loss function
  • Regression: Mean squared error



실습

1
2
3
4
5
6
7
from sklearn.ensemble import GradientBoostingClassifier

gb = GradientBoostingClassifier(random_state=42)
scores = cross_validate(gb, train_input, train_target,
return_train_score=True, n_jobs=4)
print(scores["train_score"].mean(), scores["test_score"].mean())
# 0.8881086892152563 0.8720430147331015



훈련세트과 테스트세트의 점수를 보니 overfitting이 거의 일어나지 않았습니다.

그리고 subsample라는 파라미터가 있는데 훈련세트의 비율을 나타냅니다. 기본값은 1.0인데, 이를 줄이면 그만큼의 비율만 사용하게 되고, 그러면 stochastic gradient descentmini batch gradient descent와 비슷합니다.

성능은 랜덤포레스트보다 높을 수 있지만, 트리를 순서대로 추가하기 때문에 훈련이 오래 걸립니다. 하나씩 훈련하기에 GradientBoostingClassifier에는 병렬 컴퓨팅 파라미터가 없습니다.

그래서 속도와 성능을 더욱 개선한 히스토그램 기반 그레디언트 부스팅이 있습니다.



히스토그램 기반 그레디언트 부스팅Histogram-based gradient boosting

  • 정형 데이터 (tabular 데이터 같은)에 대한 ML 알고리즘 중 인기가 가장 높습니다.
  • 이 모델은 input feature를 256개의 구간으로 나누어서, Node를 분할 시 최적 분할을 빠르게 찾을 수 있습니다.
  • HistGradientBoostingClassifier는 일반적으론 default에서 성능이 좋습니다.
  • tree의 개수를 지정하는데에는 n_estimators 대신 max_iter를 사용합니다. 성능을 높이려면 max_iter를 사용합시다.

실습

1
2
3
4
5
6
7
8
9
# sklearn에서는 아직 테스트 중입니다. enable_hist_gradient_boosting가 필요합니다.
from sklearn.experimental import enable_hist_gradient_boosting
from sklearn.ensemble import HistGradientBoostingClassifier

hgb = HistGradientBoostingClassifier(random_state=42)
scores = cross_validate(hgb, train_input, train_target,
return_train_score=True)
print(scores["train_score"].mean(), scores["test_score"].mean())
# 0.9321723946453317 0.8801241948619236

feature importance를 확인해보겠습니다. 여기서는 permutation_importance라는 함수를 사용해보겠습니다. 특성을 랜덤하게 섞고 모델의 성능이 변화하는지 관찰후 어느 특성이 중요한지 계산합니다. 이 모델이 아닌 다른 모델에도 사용 가능합니다.


permutation_importance

permutation_importance의 return값은 특성 중요도, 중요도의 평균, 표준 편차를 담고 있습니다. 중요도의 비율은 랜덤포레스트와 비슷합니다.

1
2
3
4
5
6
7
from sklearn.inspection import permutation_importance

hgb.fit(train_input, train_target)
result = permutation_importance(hgb, train_input, train_target,
n_repeats = 10, random_state=42, n_jobs = 4)
print(result.importances_mean)
# [0.08876275 0.23438522 0.08027708]

테스트 셋 확인

1
2
hgb.score(test_input, test_target)
# 0.8723076923076923

마지막으로 scikit-learn이 아닌 다른 히스토그램 기반 그레디언트 부스팅을 구현한 라이브러리는 xgboost와 lightgb이 있습니다. scikit-learn의 cross validation 함수에서도 사용할 수 있습니다.

xgboost는 다양한 부스팅 알고리즘을 제공해줍니다.

lightGBM은 마이크로소프트에서 만들었는데, 빠르고 최신기술을 많이 적용해 인기가 좋습니다.



이번에는 decision tree, cross validation, ensemble 알고리즘에 대해 알아보았습니다. 시간이 될때마다 원리와 수식을 추가로 공부하는게 도움이 될 것 같습니다.

다음에는 clustering 및 PCA 같은 비지도 학습에 대해 정리하겠습니다.

읽어주셔서 감사합니다 👋



p.s.


2주차 커피와 3주차 크로플 선물 감사합니다!