본문 바로가기
혼공학습단

[혼공머신] 4주차_트리 알고리즘과 앙상블 학습

by netsgo 2024. 1. 29.
 

혼자 공부하는 머신러닝+딥러닝 - 예스24

- 혼자 해도 충분하다! 1:1 과외하듯 배우는 인공지능 자습서 이 책은 수식과 이론으로 중무장한 머신러닝, 딥러닝 책에 지친 ‘독학하는 입문자’가 ‘꼭 필요한 내용을 제대로’ 학습할 수 있

www.yes24.com

 

트리 알고리즘(Tree  Algorithm)


결정 트리(Decision Tree) 모델은 스무고개와 비슷하다. 데이터를 잘 나눌 수 있는 질문을 하나씩 던져서 정답과 맞춰가는 것이다. 사이킷런에서는 결정 트리 알고리즘을 위해 DecisionTreeClassiger 클래스를 제공한다.

 

# 예시코드

from sklearn.tree import DecisionTreeClassifier

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

print(dt.score(train_scaled, train_target)) # 훈련 데이터, 실제 결정 트리는 표준화 전처리 과정이 필요 없다.
print(dt.score(test_scaled, test_target)) # 테스트 데이터, 실제 결정 트리는 표준화 전처리 과정이 필요 없다.

 

또한 사이킷런은 plot_tree() 함수를 사용해 결정 트리를 이해하기 쉬운 트리 그림으로 출력해준다.

import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

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

 

plot_tree()의 결과

 

이 구조가 나무를 거꾸로 세워놓은 형상이기 때문에 결정 트리라는 이름으로 불리운다. 결정 트리에서 가장 위의 노드를 root node, 가장 아래 노드를 leaf node라고 한다.

import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

plt.figure(figsize = (6, 3.5))
plot_tree(dt, max_depth = 1, filled = True, feature_names = ['alcohol', 'sugar', 'pH'])
plt.show()

max_depth, filled 매개변수 활용
각 노드의 내용

 

결정 트리는  leaf node에서 가장 많은 클래스가 예측 클래스가 된다. 만약 위의 예시에서 분류를 멈춘다면 왼쪽  node의 샘플과 오른쪽 node의 샘플은 모두 양성 클래스가 더 많으므로 두 node의 샘플은 모두 양성 클래스로 예측될 것이다.

 

불순도(Gini impurity)


DecisionTreeClassifier() 클래스는 각 node에서 데이터를 분할할 기준을 정할 때 criterion 매개변수에 입력된 값을 기준으로 상요하며 기본값은 'gini'이다. gini impurity는 클래스의 비율을 제곱해서 더한 다음 1에서 빼면 된다. 다중 클래스에서는 클래스만 많을 뿐 계산하는 방법은 동일하며 결정 트리는 부모 node와 자식 node의 불순도 차이가 가능한 크도록 트리를 성장시킨다. 부모 node와 자식 node의 불순도 차이는 정보 이득이라고 부른다.

 $$gini \ impurity = 1 - ((negative \ class)^2 + (positive \ class)^2)$$

 

결정 트리는 어떤 특성이 가장 유용한지를 나타내는 특성 중요도를 계산해 준다. 특성 중요도는 feature_importances_ 속성에 저장되어 있다. 특성 중요도는 각 노드의 정보 이득과 전체 샘플에 대한 비율을 곱한 후 특성별로 더하여 구해진다.

 

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

print(dt.feature_importances_)
[0.12345626 0.86862934 0.0079144 ]

 

가지치기


결정 트리는 무작정 끝까지 자라는 트리가 만들어지지 않도록 하기 위해서 가지치기를 해야 한다. 가지치기를 하는 가장 간단한 방법은 DecisionTreeClassifier() 클래스의 max_depth를 지정하는 것이다.

 

검증 데이터(Validation Data Set)


모델을 여러 번 평가할 때 테스트 데이터를 사용하게 되면 모델이 테스트 데이터에 맞춰지게 되고 결국 일반화 성능이 나빠지게 된다. 테스트 데이터를 사용하지 않고 모델을 평가하기 위해 검증 데이터(validation data set)를 사용한다. 검증 데이터는 훈련 데이터의 일부를 떼어 내어 만들 수 있다.

훈련 데이터가 아주 많다면 검증 데이터는 몇 %만으로도 충분하다.

 

훈련 데이터에서 모델을 훈련하고 검증 데이터로 모델을 평가한다. 이런 식으로 테스트하고 싶은 매개변수(하이퍼 파라미터)를 바꿔가며 가장 좋은 모델을 고른다. 그다음 이 매개변수를 사용해 마지막에 테스트 데이터에서 최종 점수를 평가한다.

 

교차 검증(Cross Validation)


그리드 서치(Grid Search)


검증 데이터를 만드느라 훈련 데이터가 줄어들게 되는데 보통 훈련에 사용하는 데이터가 많을수록 좋은 모델이 만들어진다. 따라서 교차 검증(cross validation)을 이용하면 훈련에 더 많은 데이터를 사용하고 안정적인 검증 점수를 얻을 수 있다.

3-fold corss validation

 

교차 검증을 위해 훈련 데이터를 몇 세트로 나누었는지 설명하기 위해 k-fold cross validation이라는 표현을 사용한다. 보통 5- fold cross validation이나 10- old cross validation을 많이 사용한다. 이렇게 하면 데이터의 80~90%까지 훈련에 사용할 수 있다. 검증 데이터가 줄어들기는 하지만 각 폴드에서 계산한 검증 점수를 평균하기 때문에 안정된 점수를 얻을 수 있다. 사이킷런에서는 cross_validate() 함수를 제공한다. cross_validate() 함수 자체는 데이터를 섞어주지 않기 때문에 train_test_split() 함수를 사용하거나 KFold 분할기를 사용해야 하며 KFold 분할기를 하는 경우 StraitifiedKFold 클래스의 n_splits, shufle 매개변수를 지정한다.

from sklearn.model_selection import StratifiedKFold
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_validate

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

# cross_validate() 함수는 딕셔너리를 반환한다.
# {'fit_time': array([0.00685358, 0.00554466, 0.00553083, 0.00536919, 0.00555754,
#         0.00654793, 0.00560236, 0.00954437, 0.00596905, 0.00557017]),
#  'score_time': array([0.00203514, 0.00161314, 0.00154853, 0.00145817, 0.00147057,
#         0.00182366, 0.00164151, 0.00197268, 0.00154638, 0.00164247]),
#  'test_score': array([0.82307692, 0.86153846, 0.80769231, 0.83653846, 0.83461538,
#         0.83653846, 0.85      , 0.8150289 , 0.83815029, 0.83236994])}
        
print(np.mean(scores['test_score']))

 

랜덤 서치(Random Search)


매개변수의 값을 정할 때 특정한 값을 사용할 근거는 없다. 그렇기 때문에 매개변수를 샘플링할 수 있는 확률 분포 객체를 전달하는 랜덤 서치(random search)를 사용하면 편리하다. 싸이파이의 stats 서브 패키지에 있는 uniform, randint 클래스를 사용하면 주어진 범위에서 고르게 값을 뽑을 수 있다.

import pandas as pd
import numpy as np
from scipy.stats import uniform, randint

rgen = randint(0, 10)
np.unique(rgen.rvs(1000), return_counts = True)

# (array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
#  array([ 91,  93, 108, 112, 103,  81, 101, 101,  95, 115]))

ugen = uniform(0, 1)
np.unique(ugen.rvs(10), return_counts = True)

# (array([0.05239922, 0.26112497, 0.40945341, 0.50339499, 0.56461151,
#         0.63353215, 0.64358641, 0.75029976, 0.93056783, 0.9606566 ]),
#  array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]))

 

탐색할 매개변수의 딕셔너리를 만든다.

params = {'min_impurity_decrease':uniform(0.0001, 0.001),
	  'max_depth':randint(20, 50),
          'min_samples_split':randint(2, 25),
          'min_samples_leaf':randint(1, 25)}
          
from sklearn.model_selection import RandomizedSearchCV

dt = DecisionTreeClassifier()
gs = RandomizedSearchCV(dt, param_distributions = params, n_iter = 100, n_jobs = -1)
gs.fit(train_input, train_target)

 

위의 매개변수 범위에서 총 100번을 샘플링(n_iter)하여 교차 검증을 수행하고 최적의 매개변수 조합을 찾는다. 그리드 서치보다 교차 검증 수를 줄이면서 넓은 영역을 효과적으로 탐색할 수 있다.

 

앙상블 학습(Ensemble Learning)


앙상블 학습(Ensemble Learning)은 여러 개의 학습 알고리즘을 사용하거나, 같은 알고리즘을 사용하지만 학습 시킬 때 마다 다른 데이터 셋을 사용하여 여러 개의 모델을 학습시키는 방법을 의미한다. 이렇게 학습된 여러 모델을 결합해서 최종 결론을 도출하는데, 이 방법은 단일 모델을 사용하는 것보다 일반적으로 더 나은 성능을 보인다. 앙상블 학습은 크게 배깅(Bagging), 부스팅(Boosting), 스태킹(Stacking) 등의 방법으로 나눌 수 있다.

 

랜덤 포레스트(Random Forest)


랜덤 포레스트(Random Forest)는 결정 트리를 랜덤하게 만들어 결정 트리의 숲을 만든다. 그리고 각 결정 트리의 예측을 사용해서 최적의 예측을 만든다.

 

랜덤 포레스트는 각 트리를 훈련하기 위한 데이터를 랜덤하게 만드는데, 이렇게 만들어진 샘플을 부트스트랩 샘플(bootstrap sample)이라고 부른다. 또한 각 노드를 만들 때 전체 특성 중 일부 특성을 무작위로 고른 다음 이 중에서 최선의 분할을 찾는데, 분류에서는 기본적으로 전체 특성 개수의 제곱근만큼의 특성을 사용한다. 사이킷런의 랜덤 포레스트는 기본적으로 100개의 결정 트리를 훈련하며 각 트리의 클래스별 확률을 평균하여 가정 높은 확률을 가진 클래스를 예측으로 삼는다.

 

랜덤 포레스트는 훈련 데이터를 만들 때 중복을 허용한 샘플링을 하기 때문에 부트스트랩 샘플에 포함되지 않고 남는 샘플이 존재하게 된다. 이런 샘플을 OOB(Out Of Bag) 샘플이라고 한다. 이 OOB 샘플을 마치 검증 데이터와 같이 활용하여 결정 트리를 평가할 수 있다. OOB를 이용하면 교차 검증을 대신할 수 있기 때문에 결과적으로 훈련 데이터에 더 많은 샘플을 사용할 수 있게 된다.

from sklearn.model_selection import cross_validate
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_jobs = -1, random_state = 42)
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']))

 

엑스트라 트리(Extra Tree)


엑스트라 트리(Extra Tree)는 랜덤 포레스트와 매우 비슷하지만 부트스트랩 샘플을 사용하지 않는다. 즉, 각 트리를 만들 때 전체 훈련 데이터를 사용한다는 차이점을 가지고 있다. 노드를 분할할 때는 가장 좋은 분할을 찾는 것이 아니라 무작위로 분할한다. 하나의 결정 트리를 학습할 때 특성을 무작위로 분할한다면 성능이 낮아지겠지만 많은 트리를 앙상블하기 때문에 검증 데이터의 점수를 높이는 효과가 있다.

from sklearn.ensemble import ExtraTreesClassifier
from sklearn.model_selection import cross_validate
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']))

 

그레이디언트 부스팅(Gradient Boosting)


그레이디언트 부스팅(Gradient Boosting)는 깊이가 얕은 결정 트리를 사용하여 이전 트리의 오차를 보완하는 방식의 앙상블 학습이다. 사이킷런의 GradientBoostingClassifier는 기본적으로 깊이가 3인 결정 트리를 100개 사용한다.

from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_validate
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']))

 

그레이디언트 부스팅은 훈련에 사용할 훈련 데이터의 비율을 정하는 subsample 매개변수를 가지고 있다. 이 매개변수의 기본값은 1.0으로 전체 훈련 데이터를 사용한다. 하지만 subsample이 1보다 작으면 훈련 데이터의 일부만 사용하게 되는데 마치 경사 하강법에서 일부 샘플을 랜덤하게 사용하는 확률적 경사 하강법이나 미니배치 경사 하강법과 비슷하다고 할 수 있다.

 

히스토그램 기반 그레이디언트 부스팅(Histogram_based Gradient Boosting)


히스토그램 기반 그레이디언트 부스팅(Histogram_based Gradient Boosting)은 입력 특성을 256개의 구간으로 나눈다. 따라서 노드를 분할할 때 최적의 분할을 매우 빠르게 찾을 수 있다. 히스토그램 기반 그레이디언트 부스팅은 256개의 구간 중에서 하나를 떼어 놓고 누락된 값을 위해서 사용하기 때문에 입력에 누락된 특성이 있더라도 이를 따로 전처리할 필요가 없다.

from sklearn.experimental import enable_hist_gradient_boosting
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import cross_validate
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']))

 

HistGradientBoostingClassifier는 트리의 개수를 지정할 때 n_estimators대신 부스팅 횟수를 지정하는 max_iter를 사용한다.