티스토리 뷰

지금까지 지도 학습을 진행하면서 k-최근접 이웃, 선형 회귀부터 로지스틱 회귀, 확률적 경사 하강법 알고리즘을 사용한 분류기와 결정 트리 모델까지 다양한 학습 알고리즘 모델을 다뤘다. 

 

직전 포스팅에서는 교차 검증을 통한 하이퍼 파리미터 튜닝까지 익히며, 이제는 꽤나 머신러닝과 친해진 거 같다는 생각이 든다.

 

그럼 지금까지 학습한 모델 중 가장 좋은 알고리즘은 무엇일까? 

 

물론 데이터의 유형이나 예측해야 하는 값에 따라 매번 다르겠지만, 분명히 가장 좋은 성능을 내는 모델이 존재할 것이다.

 

지금까지 예제를 실습하면서 다룬 데이터는 전부 CSV 파일로 정리된 데이터였으며, 이는 정형 데이터라고 부른다. 

 

어떤 구조를 가지고 있는 데이터라는 뜻인데, 이와 반대되는 개념을 가진 데이터를 우리는 비정형 데이터라고 부른다

 

정형 데이터를 다루는데 가장 뛰어난 성능을 내는 알고리즘이 바로 앙상블 학습이라는 알고리즘이다.

 

앙상블 알고리즘은 대부분 결정 트리를 기반으로 만들어져 있으며, 여러 개의 분류기를 생성하고, 그 예측을 모두 결합하여 지금까지의 모델보다 더욱 정확한 예측을 도출하는 기법이라고 할 수 있다.

 

이번 포스팅에서는 이 정형 데이터를 다루는 끝판왕 모델인 앙상블 학습 알고리즘에 대하여 알아보려고 한다.

 

바로 들어가보자.


랜덤 포레스트

랜덤 포레스트는 앙상브 학습의 대표 주자 중 하나로 안정적인 성능 덕분에 널리 사용되고 있는 모델이다. 

 

이름 자체로 유추할 수 있듯이 랜덤 포레스트는 결정 트리를 랜덤하게 만들어 결정트리의 숲을 만든다. 그리고 각 결정 트리의 예측을 사용해 최종 예측을 만든다.

 

먼저 랜덤 포레스트는 각 트리를 훈련하기 위한 데이터를 랜덤하게 만드는데, 이 데이터를 만드는 방법이 독특하다. 

입력한 훈련 데이터에서 랜덤하게 샘플을 추출하여 훈련 데이터를 만드는데, 이때 한 샘플이 중복되어 추출될 수 있다.

 

즉, 1000개의 샘플이 있다면 1개의 샘플을 뽑고 값을 확인한 뒤 다시 데이터 세트에 넣어 랜덤으로 하나를 뽑는 방식이다. 

 

이렇게 중복을 허용하고 만들어진 샘플을 부트스트랩 샘플이라고 한다.

 

또한 각 노드를 분할할 떄 전체 특성 중에서 일부 특성을 무작위로 고른 다음 이 중에서 최선의 분할을 찾는다. 

 

분류 모델인 RandomForestClassifier는 기본적으로 전체 특성 개수의 제곱근만큼의 특성을 선택한다. 

즉, 4개의 특성이 존재하면 그 중 2개를 랜덤하게 선택하여 사용하는 것이다. 

 

반면에 회귀 모델인 RandomForestRegressor는 전체 특성을 모두 사용한다. 

 

사이킷런의 랜덤 포레스트는 기본적으로 100개의 결정 트리를 이런 방식으로 훈련시킨다.

 

그 다음 분류일 때는 각 트리의 클래스별 확률을 평균하여 가장 높은 확률을 가진 클래스를 예측으로 삼는다. 

회귀일 때는 단순히 각 트리의 예측을 평균한다.

 

이제, 랜덤 포레스트 모델을 직접 코드와 함께 알아보자.

 

데이터는 직전 포스팅에서 사용했던 와인 분류 데이터를 사용한다.

import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 데이터 준비
wine = pd.read_csv('https://bit.ly/wine_csv_data')

# input 과 target 데이터로 나눔
data = wine[['alcohol', 'sugar', 'pH']].to_numpy()
target = wine['class'].to_numpy()

# test_size = 0.2 -> 샘플 갯수가 충분히 많으므로, 20% 정도만 테스트 세트로 나눈다.
train_input, test_input, train_target, test_target = train_test_split(data, target, test_size = 0.2, random_state=42)

from sklearn.model_selection import cross_validate
from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_jobs=-1, random_state=42)

# return_train_score = True로 지정하면 검증 점수 뿐만 아니라 훈련 세트에 대한 점수도 함께 반환한다.
scores = cross_validate(rf, train_input, train_target, return_train_score=True, n_jobs=-1)

print(np.mean(scores['train_score']), np.mean(scores['test_score']))
# 0.9973541965122431 0.8905151032797809

RandomForestClassifier 클래스를 불러와 해당 모델을 만들고, cross_validate 함수로 훈련 데이터와 검증 데이터에 대한 점수의 평균을 구해봤다.

 

출력된 결과를 보면, 훈련 세트에 다소 과대적합되었지만, 테스트 세트의 점수 또한 상당히 높은 걸 확인할 수 있다.

 

아마 하이퍼 파라미터 튜닝을 하면 과대적합을 해결할 수 있겠지만, 우선은 넘어가자. 

 

이제 랜덤 포레스트가  사용한 각 특성의 중요도를 확인해보자. 

rf.fit(train_input, train_target)
print(rf.feature_importances_)
# [0.23167441 0.50039841 0.26792718]

이전 포스팅에서 결정 트리의 특성 중요도를 확인했을 때와 비교하면 당도 특성의 중요도가 많이 줄어들고, 다른 특성의 중요도가 상승했다.

 

랜덤 포레스트가 특성의 일부를 랜덤하게 선택하여 결정 트리를 훈련시키기 때문이다.

 

이는 과대적합을 줄이고 일반화 성능을 높이는 데 도움이 된다. 

 

랜덤 포레스트는 자체적으로 모델을 평가하는 점수를 얻을 수 있다. 

 

부트스트랩 샘플은 중복을 허용하기 때문에 훈련이 끝나고 샘플에 포함되지 않고 남아있는 샘플이 있다. 이런 샘플을 OOB (out of bag) 샘플이라고 하는데, 이 남는 샘플을 활용하여 랜덤 포레스트는 각 결정 트리의 OOB 점수를 평균하여 출력한다.

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

OOB 점수를 사용하면 교차 검증을 대신할 수 있어서 결과적으로 훈련 세트에 더 많은 샘플을 사용할 수 있다.

 

 

이제 또 다른 앙상블 학습 알고리즘인 엑스트라 트리 모델을 알아보자.


엑스트라 트리

액스트라 트리는 랜덤 포레스트와 매우 비슷하게 동작한다. 

 

기본적으로 100개의 결정 트리를 훈련한다. 랜덤 포레스트는 동일하게 결정트리가 제공하는 대부분의 매개변수를 지원한다.

 

또한 전체 특성 중에 일부 특성을 랜덤하게 선택하여 노드를 분할하는데 사용한다.

 

랜덤 포레스트와 엑스트라 트리의 차이점은 부트스트랩 샘플의 사용 여부이다. 엑스트라 트리 모델은 부트스트랩 샘플을 사용하지 않고 결정 트리를 만들 때 전체 훈련 세트를 사용한다. 

 

대신, 노드를 분할할 때 가장 좋은 분할을 찾는 것이 아니라 무작위로 분할한다.

 

사이킷런에서는 ExtraTreesClassifier 클래스를 사용하여 엑스트라 트리 모델을 제공한다.

from sklearn.ensemble import ExtraTreesClassifier

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

보통 엑스트라 트리가 무작위성이 좀 더 크기 때문에 랜덤 포레스트보다 더 많은 결정 트리를 훈련해야 한다. 하지만 랜덤하게 노드를 분할하기 때문에 빠른 계산 속도라는 장점을 가지고 있다.

 

마찬가지로 특성 중요도 또한 확인할 수 있다.

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

 

랜덤 포레스트와 엑스트라 트리 알고리즘은 비슷한듯 다르다. 이제 이 둘과는 또 다른 방식을 사용하는 앙상블 학습을 알아보자.


그레이디언트 부스팅

그레디언트 부스팅은 깊이가 얕은 결정 트리를 사용하여 이전 트리의 오차를 보완하는 방식으로 앙상블을 하는 방법이다. 

깊이가 얕은 결정 트리를 사용하기 때문에 과대적합에 강하고 일반적으로 높은 일반화 성능을 기대할 수 있다.

 

그레디언트라는 이름에 맞게 경사 하강법을 사용하여 트리를 앙상블에 추가한다. 

 

분류에서는 로지스틱 손실 함수를 사용하고, 회귀에서는 평균 제곱 오차 함수를 사용한다.

 

즉, 결정 트리를 계속 추가하면서 가장 낮은 곳을 찾아 이동한다는 것이다. 

 

바로 코드와 함께 알아보자.

from sklearn.ensemble import GradientBoostingClassifier

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

정말로 과대적합이 거의 일어나지 않았다. 그레디언트 부스팅은 결정 트리의 개수를 늘려도 과대적합에 매우 강하기 때문이다. 결정 트리의 개수를 늘리면 성능은 더 향상된다.

 

# n_estimators -> 결정 트리의 개수, learning_rate -> 학습률(기본값 : 0.1)
gb = GradientBoostingClassifier(n_estimators=500, learning_rate=0.2, random_state=42)
scores = cross_validate(gb, train_input, train_target, return_train_score=True, n_jobs=-1)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))
# 0.9464595437171814 0.8780082549788999

결정 트리의 개수를 5배나 늘렸음에도 과대적합을 상대적으로 잘 억제하고 있다. 

 

마찬가지로 특성 중요도를 한번 확인해보자.

gb.fit(train_input, train_target)
print(gb.feature_importances_)
# [0.15853457 0.68010884 0.1613566 ]

위 2개의 모델보다 조금 더 일부 특성에 집중하는 것을 확인할 수 있다. 

 

또한 이 모델은 subsample 이라는 매개변수를 하나 더 가지고 있는데, 기본값은1.0으로 전체 훈련 세트를 모두 사용하는 것이고,  1보다 작으면 훈련 세트의 일부를 사용한다. 

 

일반적으로 그레디언트 부스팅이 랜덤 포레스트보다 조금 더 높은 성능을 얻을 수 있지만, 순서대로 트리를 추가하기 때문에 훈련 속도가 느리다. 

 

이 그레디언트 부스팅의 속도와 성능을 더욱 개선한 모델이 바로 히스토그램 기반 그레디언트 부스팅 모델이다.


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

히스토그램 기반 그레디언트 부스팅 알고리즘은 정형 데이터를 다루는 머신러닝 알고리즘 중에 가장 인기가 높은 알고리즘이다. 

 

이 모델은 먼저 입력 특성을 256개의 구간으로 나눈다. 따라서 노드를 분할할 때 최적의 분할을 매우 빠르게 찾을 수 있다. 

 

또한 256개의 구간 중에서 하나를 떼어 놓고 누락된 값을 위해서 사용한다. 따라서 입력에 누락된 특성이 있더라도 이를 따로 전처리할 필요가 없다는 것이다!

 

바로 한번 확인해보자.

from sklearn.ensemble import HistGradientBoostingClassifier

# HistGradientBoostingClassifier는 트리의 개수를 지정하는데 n_estimators 대신에 max_iter를 사용한다.
# 하지만 기본 매개변수에서도 안정적인 성능을 얻을 수 있기에 지정하지 않는다.
hgb = HistGradientBoostingClassifier(random_state=42)
scores = cross_validate(hgb, train_input, train_target, return_train_score=True)
print(np.mean(scores['train_score']), np.mean(scores['test_score']))
# 0.9321723946453317 0.8801241948619236

wow! 과대적합을 잘 억제하면서 그레디언트 부스팅보다 조금 더 높은 성능을 제공한다! 

 

특성 중요도 또한 확인해보자.

 

이번에는 feature_important_ 속성이 아닌 permutation_importance() 함수를 사용하자. 

이 함수는 특성을 랜덤하게 섞어서 모델의 성능이 변화하는지를 관찰하여 어떤 특성이 중요한 지를 계산한다.

from sklearn.inspection import permutation_importance

hgb.fit(train_input, train_target)
# n_repeats -> 특성을 랜덤하게 섞을 횟수, 기본값 = 5
result = permutation_importance(hgb, train_input, train_target, n_repeats=10, random_state=42, n_jobs=-1)
print(result.importances)
# [[0.08793535 0.08350972 0.08908986 0.08312488 0.09274581 0.08755051
#   0.08601116 0.09601693 0.09082163 0.09082163]
#  [0.22782374 0.23590533 0.23936887 0.23436598 0.23725226 0.23436598
#   0.23359631 0.23398114 0.23994612 0.22724649]
#  [0.08581874 0.08601116 0.08062344 0.07504329 0.08427939 0.07792957
#   0.07234943 0.07465846 0.08139311 0.08466423]]
# [0.08876275 0.23438522 0.08027708]
# [0.00382333 0.00401363 0.00477012]

print(result.importances_mean)
# [0.08876275 0.23438522 0.08027708]

print(result.importances_std)
# [0.00382333 0.00401363 0.00477012]

permutation_importance() 함수가 반환하는 객체는 반복하여 얻은 특성 중요도, 평균, 표준편차를 담고 있다. 

 

결과를 보면 그레디언트 부스팅과 비슷하게 당도에 조금 더 집중하고 있다는 것을 알 수 있다. 

 

이제 최종적으로 테스트 세트의 스코어를 확인해보자.

print(hgb.score(test_input, test_target))
# 0.8723076923076923

약 87% 의 정확도를 얻었다! 확실히 결정 트리 하나만 사용하는 것보다 좋은 결과를 얻었다.


앙상블 학습은 정형 데이터에서 가장 많이 사용되는 알고리즘이기 때문에 매우 중요한 모델이라고 할 수 있다.

 

반드시 복습하여 익히도록 하자.

 

 

끝!

Comments