Post

[혼공머신] 4주차 05-3 트리의 앙상블

[혼공머신] 4주차 05-3 트리의 앙상블

본 게시글은 한빛미디어의 혼자 공부하는 머신러닝+딥러닝을 바탕으로 작성되었습니다.

정형 데이터와 비정형 데이터

우리는 data를 분석할 때 다양한 데이터를 보게 된다. 그런데 지금까지 봤던 데이터들은 아무 이쁘게 정리된 데이터들이다. 표로 정리되어 보기도 쉽고, 처리하기도 쉬운 정형화된 데이터들이다. 이러한 데이터는 지금까지 다양한 모델을 사용해서 쉽게 학습할 수 있었다. 이러한 데이터를 우리는 정형 데이터(Structured Data)라고 한다. 그런데 세상에는 이러한 데이터들만 존재하지는 않는다. 사진도 있고, 동영상도 있고, 소리도 있다. 이러한 데이터는 비정형 데이터(Unstructured Data)라고 한다.

지금까지 우린 structured data를 바탕으로 학습을 진행했는데, 이러한 structured data에 사용하기 좋은 알고리즘이 있다. 바로 앙상블 학습(Ensamble Learning)이다.

랜덤 포레스트

랜덤 포레스트(Random Forest)는 ensamble 기법 중 대표적인 알고리즘이다. Random forest 기법을 간단하게 설명하자면 decision tree를 랜덤하게 만들어서 하나의 숲을 만든 다음 이것들을 사용해 최종 예측을 만든다.

예를 들어 100개의 sample이 있는 곳에서 10개의 sample을 뽑는다고 할 때, 1개를 뽑고 뽑은 1개를 다시 넣은 다음 다시 1개를 또 뽑는 방식으로 10개를 뽑는 것이다. 즉, 복원 추출을 한다는 의미이다. 이러한 방식으로 샘플을 만드는 방식을 부트스트랩 샘플(Bootstrap Sample)이라고 한다. 일반적으로 bootstrap sample은 training set의 크기와 동일하게 만든다.

그리고 이전에 decision tree를 살펴봤는데 여러 node가 있었던 것을 기억할 것이다. 이것처럼 random forest에서도 각 node를 분할 할 때, 전체 feature 중에서 일부 feature를 랜덤하게 고른 다음 최선의 분할 방법을 찾는다. 이때, RandomForestClassifier는 전체 특성 개수의 제곱근만큼 특성을 선택하고 RandomForestRegressor는 전체 특성을 선택한다. 이렇게 해서 학습을 진행하는데, 학습 후 classifier의 경우 class별 확률의 평균을 구해 가장 높은 확률을 가진 class로 예측하고, regressor의 경우 확률을 예측해준다.

이제 코드로 한번 살펴보자. 결과를 보면 overfitting이 된 것 같다는 느낌이 온다. 하지만 이번엔 그냥 모델을 살펴보는 게 목표이므로 그냥 볼 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_validate
from sklearn.ensemble import RandomForestClassifier

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

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

X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=42)

model = RandomForestClassifier(n_jobs=-1, random_state=42)
scores = cross_validate(model, X_train, y_train, return_train_score=True, n_jobs=-1)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))

0.9973541965122431 0.8905151032797809

그리고 지금까지 해왔던 다른 여러 모델과 같이 feature의 중요도를 볼 수 있다.

1
2
model.fit(X_train, y_train)
model.feature_importances_

array([0.23167441, 0.50039841, 0.26792718])

또한 random forest에는 자체적으로 모델을 평가하는 기능이 있다. 아까 random forest에서는 bootstrap sampling을 한다고 했었다. 그런데 이러한 방식으로 복원추출이므로 선택되지 않는 sample이 있을 수 있다. 이러한 샘플을 OOB 샘플(Out of Bag Sample)이라고 하는데 이처럼 학습되지 않고 남는 샘플을 활용하여 평가를 하는 것이다.

1
2
3
model = RandomForestClassifier(oob_score=True, n_jobs=-1, random_state=42)
model.fit(X_train, y_train)
model.oob_score_

0.8934000384837406

엑스트라 트리

엑스트라 트리(Extra Tree)의 경우 random forest와 비슷하게 동작하는데, extra tree의 차이는 bootstrap sampling을 사용하지 않는다는 것이다. 코드는 아래와 같이 간단하게 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_validate
from sklearn.ensemble import ExtraTreesClassifier

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

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

X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=42)

model = ExtraTreesClassifier(n_jobs=-1, random_state=42)
scores = cross_validate(model, X_train, y_train, return_train_score=True, n_jobs=-1)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))

0.9974503966084433 0.8887848893166506

그레디언트 부스팅

그레디언트 부스팅(Gradient Boosting)은 이전의 방식과는 조금 다른 방식이다. 깊이가 낮은 decision tree를 사용해 이전 tree의 오차를 보완하는 방식의 ensamble 방법이다. 이렇게만 말하면 잘 이해가 어려울 수 있는데, 기본적으로 깊이가 3인 decision tree를 100개 사용하는 방식이다. 깊이가 낮기에 overfitting의 우려를 낮추고 generalization에 집중할 수 있게 된다. 이름에서도 알 수 있듯 경사하강법을 활용한다. 아래의 코드를 통해 결과를 살펴봐도 이전의 결과와는 달리 overfitting을 의심할만한 증거가 크게 보이지 않는다고 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_validate
from sklearn.ensemble import GradientBoostingClassifier

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

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

X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=42)

model = GradientBoostingClassifier(random_state=42)
scores = cross_validate(model, X_train, y_train, return_train_score=True, n_jobs=-1)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))

0.8881086892152563 0.8720430147331015

Gradient boosting에는 subsample이라는 새로운 하이퍼파라미터가 추가되는데, 1로 설정하면 전체 training set을 사용하고, 이보다 작으면 일부를 사용하게 된다. 이것은 확률적 경사하강법과 미니배치 경사하강법과 같이 일부를 사용해 경사하강법을 진행하는 방식이라고 이해하면 된다.

일반적으로 gradient boosting이 random forest보다 성능이 좋지만, 다른 여러 모델과 마찬가지로 성능이 좋다는 건 모델이 복잡하다는 것이고, 모델이 복잡하다는 것은 시간과 비용이 많이 소요된다는 것이다.

히스토그램 기반 그레디언트 부스팅

히스토그램 기반 그레디언트 부스팅(Histogram-based Gradient Boosting)의 경우, 입력 구간을 256개로 나눠 node에서 최적의 분할을 빠르게 찾을 수 있게 한다. 이전 gradient boosting 모델들과 비교해 overfitting이 조금 억제되고 성능 향상이 됨을 살펴볼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_validate
from sklearn.ensemble import HistGradientBoostingClassifier

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

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

X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=42)

model = HistGradientBoostingClassifier(random_state=42)
scores = cross_validate(model, X_train, y_train, return_train_score=True, n_jobs=-1)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))

0.9321723946453317 0.8801241948619236

XGBoost, LightGBM

Gradiant boosting 기법을 사용하는 라이브러리에는 다른 것도 있다. 대표적인 것이 XGBoost이다. 이 XGBoost는 아마 kaggle이나 DACON 등에서도 많이 보았을 것이다. XGBoost의 경우 코랩에서 사용 가능하고, cross_validate()와 같이 사용할 수 있고, 여러 부스팅 알고리즘을 활용할 수도 있다. 아마 분석을 하게 된다면 sklearn과 함께 가장 많이 사용하게 될 라이브러리일 것이라고 생각한다. 또한, 비슷한 것으로 lightgbm도 있다. 이러한 것들을 코드로 살펴보면 다음과 같다. 자세한 내용은 나중에 따로 다루도록 해야겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_validate
from xgboost import XGBClassifier

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

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

X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=42)

model = XGBClassifier(tree_method='hist', random_state=42)
scores = cross_validate(model, X_train, y_train, return_train_score=True, n_jobs=-1)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))

0.9558403027491312 0.8782000074035686

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split, cross_validate
from lightgbm import LGBMClassifier

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

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

X_train, X_test, y_train, y_test = train_test_split(data, target, test_size=0.2, random_state=42)

model = LGBMClassifier(random_state=42)
scores = cross_validate(model, X_train, y_train, return_train_score=True, n_jobs=-1)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))

0.935828414851749 0.8801251203079884

This post is licensed under CC BY 4.0 by the author.