본문 바로가기

문돌이 존버/데이터 분석

핸즈온 머신러닝 2 복습하기(챕터 7: 앙상블 학습과 랜덤 포레스트)

반응형

일련의 예측기(분류나 회귀 모델)로부터 예측을 수집하면 가장 좋은 모델 하나보다도 더 좋은 예측을 얻을 수 있다. 이를 앙상블 학습(Ensemble Learning)이라 하며, 의사결정나무의 앙상블을 랜덤 포레스트(Random Frest)라고 한다.

7.1 투표 기반 분류기

더 좋은 분류기를 만드는 매우 간단한 방법은 각 분류기의 예측을 모아서 가장 많이 선택된 클래스를 예측하는 것이다. 이렇게 다수결 투표로 정해지는 분류기를 직접 투표(hard voting) 분류기라고 한다. 이때 필요한 가정은 모든 분류기가 완벽하게 독립적이고 오차에 상관관계가 없어야 한다는 것이다. 

from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC

log_clf = LogisticRegression(solver="lbfgs", random_state=42)
rnd_clf = RandomForestClassifier(n_estimators=100, random_state=42)
svm_clf = SVC(gamma="scale", random_state=42)

voting_clf = VotingClassifier(
    estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)],
    voting='hard')
    
voting_clf.fit(X_train, y_train)
# 각 분류기의 테스트셋 정확도 확인
from sklearn.metrics import accuracy_score

for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)
    print(clf.__class__.__name__, accuracy_score(y_test, y_pred))

모든 분류기가 클래스의 확률을 예측할 수 있으면(즉, predict_proba() 메서드가 있으면) 개별 분류기의 예측을 평균 내어 확률이 가장 높은 클래스를 예측할 수 있다. 이를 간접 투표(soft voting)라고 하며, 확률이 높은 투표에 비중을 두기 때문에 직접 투표 방식보다 성능이 높다. 이 방식을 사용하기 위해선 voting="hard" voting="soft" 로 바꾸고 모든 분류기가 클래스의 확률을 추정할 수 있으면 된다. 

참고로 SVC는 기본값에선 클래스 확률을 제공하지 않으므로 probability 매개변수를 True로 지정해야 한다.

7.2 배깅과 페이스팅

앞서 각기 다른 훈련 알고리즘을 사용하여 다양한 분류기를 만드는 것과 달리 같은 알고리즘을 사용하지만 훈련 세트의 서브셋(subset)을 무작위로 구성하여 분류기를 각기 다르게 학습할 수도 있다. 훈련 세트에서 중복을 허용하여 샘플링하는 방식을 배깅(bagging), 중복을 허용하지 않고 샘플링하는 방식을 페이스팅(pasting)이라 한다.

모든 예측기가 훈련을 마치면 앙상블은 모든 예측기의 예측을 모아서 새로운 샘플에 대한 예측을 만든다. 수집 함수는 전형적으로 분류일 때는 통계적 최빈값(statistical mode)이고, 회귀에 대해서는 평균을 계산한다. 일반적으로 앙상블의 결과는 원본 데이터셋으로 하나의 예측기를 훈련시킬 때와 비교해 편향은 비슷하지만 분산은 줄어든다.

7.2.1 사이킷런의 배깅과 페이스팅

사이킷런은 BaggingClassifier(회귀는 BaggingRegressor)를 제공하며, 페이스팅을 사용하려면 bootstrap=False 로 지정하면 된다. 

from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier

bag_clf = BaggingClassifier(
    DecisionTreeClassifier(), n_estimators=500,
    max_samples=100, bootstrap=True, random_state=42)
bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)

부트스트래핑은 각 예측기가 학습하는 서브셋에 다양성을 증가시키므로 배깅이 페이스팅보다 편향이 조금 더 높다. 하지만 이는 예측기들의 상관관계를 줄이므로 앙상블의 분산을 감소시킨다. 전반적으로 배깅이 더 나은 모델을 만든다고 하지만 CPU 여유가 된다면 둘 다 시도해보고 비교하는 것이 좋다.

7.2.2 oob 평가

배깅을 사용하면 어떤 샘플은 한 예측기를 위해 여러 번 샘플링되고 어떤 것은 전혀 선택되지 않을 수 있다. 이때 선택되지 않은 훈련 샘플의 나머지를 oob(out-of-bag)라고 부른다. 예측기가 훈련되는 동안에는 oob 샘플을 사용하지 않으므로 검증 세트나 교차 검증을 사용하지 않고 oob 샘플을 사용해 평가할 수 있다.

사이킷런에서는 oob_score=True 로 지정하면 훈련이 끝난 후 자동으로 oob 평가를 수행한다.

bag_clf = BaggingClassifier(
    DecisionTreeClassifier(), n_estimators=500,
    bootstrap=True, oob_score=True, random_state=40)
bag_clf.fit(X_train, y_train)
bag_clf.oob_score_

oob 샘플에 대한 결정 함수의 값도 oob_decision_function_ 변수에서 확인 가능하다. 이 경우 각 훈련 샘플의 클래스 확률을 반환한다. 

bag_clf.oob_decision_function_

7.3 랜덤 패치와 랜덤 서브스페이스

BaggingClassifier는 특성 샘플링도 지원하는데, max_features, boostrap_features 두 매개변수로 조절된다. 특히 이미지와 같은 매우 고차원의 데이터셋을 다룰 때 유용하며, 훈련 특성과 샘플을 모두 샘플링하는 것을 랜덤 패치 방식(Random Patches method)이라고 한다. 훈련 샘플을 모두 사용하고(boostrap=False이고 max_samples=1.0) 특성은 샘플링하는(boostrap_features=True이고 max_features는 1.0보다 작은) 것을 랜덤 서브스페이스 방식(Random Subspaces method)이라 한다. 특성 샘플링은 더 다양한 예측기를 만들며 편향을 늘리는 대신 분산을 낮춘다.

7.4 랜덤 포레스트

사이킷런의 RandomForestClassifier 를 사용하며, 전형적으로 max_samples를 훈련 세트의 크기로 지정한다.

from sklearn.ensemble import RandomForestClassifier

rnd_clf = RandomForestClassifier(n_estimators=500, max_leaf_nodes=16, random_state=42)
rnd_clf.fit(X_train, y_train)

y_pred_rf = rnd_clf.predict(X_test)

랜덤 포레스트 알고리즘은 트리의 노드를 분할할 때 전체 특성 중에서 최선의 특성을 찾는 대신 무작위로 선택한 특성 후보 중에서 최적의 특성을 찾는 식으로 무작위성을 더 주입한다. 이로써 편향을 손해 보는 대신 분산을 낮추어 전체적으로 더 훌륭한 모델을 만들어낸다.

7.4.1 엑스트라 트리

트리를 더욱 무작위하게 만들기 위해 최적의 임곗값을 찾는 대신 후보 특성을 사용해 무작위로 분할한 다음 그중에서 최상의 분할을 선택한다. 이와 같이 극단적으로 무작위한 트리의 랜덤 포레스트를 익스트림 랜덤 트리(Extremely Randomized Trees) 앙상블이라고도 부른다. 역시 편향은 늘어나지만 분산을 낮추며, 가장 최적의 임곗값을 찾는 것이 트리 알고리즘에서 가장 시간이 많이 소요되는 작업 중 하나이므로 일반적인 램덤 포레스트보다 엑스트라 트리가 훨씬 빠르다.

7.4.2 특성 중요도

랜덤 포레스트의 장점은 특성의 상대적 중요도를 측정하기 쉽다는 것이다. 사이킷런은 어떤 특성을 사용한 노드가 (랜덤 포레스트에 있는 모든 트리에 걸쳐서) 평균적으로 불순도를 얼마나 감소시키는지 확인하여 특성의 중요도를 측정한다. 더 정확히 말하면 가중치 평균이며 각 노드의 가중치는 연관된 훈련 샘플 수와 같다. 

(참고)
의사결정나무의 특성 중요도는 노드에 사용된 특성별로
(현재 노드의 샘플 비율 x 불순도) - (왼쪽 자식 노드의 샘플 비율 x 불순도) - (오른쪽 자식 노드의 샘플 비율 x 불순도)
와 같이 계산하여 더하고, 특성 중요도의 합이 1이 되도록 전체 합으로 나누어 정규화를 한다. 여기서 샘플 비율은 트리 전체 샘플 수에 대한 비율이다.
반면, 랜덤 포레스트의 특성 중요도는 각 의사결정나무의 특성 중요도를 모두 계산하여 더한 후 트리수로 나눈 것이다.

특성 중요도는 사이킷런의 feature_importances_ 변수에 저장되어 있다.

rnd_clf = RandomForestClassifier(n_estimators=100, random_state=42)
rnd_clf.fit(mnist["data"], mnist["target"])

def plot_digit(data):
    image = data.reshape(28, 28)
    plt.imshow(image, cmap = mpl.cm.hot,
               interpolation="nearest")
    plt.axis("off")
    
plot_digit(rnd_clf.feature_importances_)

cbar = plt.colorbar(ticks=[rnd_clf.feature_importances_.min(), rnd_clf.feature_importances_.max()])
cbar.ax.set_yticklabels(['Not important', 'Very important'])

save_fig("mnist_feature_importance_plot")
plt.show()

7.5 부스팅

부스팅(boosting)은 약한 학습기를 여러 개 연결하여 강한 학습기를 만드는 앙상블 방법을 말한다. 대표적인 방법으로는 아다부스트(AdaBoost)그래디언트 부스팅(Gradient Boosting)이다.

7.5.1. 아다부스트

이전 예측기를 보완하는 새로운 예측기를 만드는 방법은 이전 모델이 과소적합했던 훈련 샘플의 가중치를 더 높이는 것이다. 경사 하강법은 비용 함수를 최소화하기 위해 한 예측기의 모델 파라미터를 조정해가는 반면 아다부스트는 점차 더 좋아지도록 앙상블에 예측기를 추가한다.

아다부스트 동작 방식

각 샘플 가중치 $w^{(i)}$는 초기에 ${1 \over m}$로 초기화된다.
첫 번째 예측기가 학습되고, 가중치가 적용된 에러율 $r_1$이 훈련 세트에 대해 계산된다.

아래는 j번째 예측기의 가중치가 적용된 에러율이다. 여기서 $\hat{y}_j^{(i)}$는 $i$번째 샘플에 대한 $j$번째 예측기의 예측이다.
예측기의 가중치 $\alpha_j$는 아래 식을 통해 계산된다. 여기서 $\eta$는 학습률 하이퍼파라미터이다. 예측기가 정확할수록 가중치가 더 높아지게 되고($r_j$가 높아지면 아래 식은 커지게 됨), 무작위로 예측하는 정도라면 가중치가 0에 가까울 것이다.
$\alpha_j=\eta log{1-r_j \over r_j}$

이후에는 아래 식을 사용해 샘플의 가중치를 업데이트한다. 즉 잘못 분류된 샘플의 가중치가 증가하는 것이다.
$\hat{y}_j^{(i)} = y^{(i)}$일 때, $w^{(i)}$
$\hat{y}_j^{(i)} \ne y^{(i)}$일 때, $w^{(i)}exp(\alpha_j)$
여기서 $i=1, 2, ..., m$

그런 다음 모든 샘플의 가중치를 정규화한다. 즉 $\sum_{i=1}^m w^{(i)}$으로 나누는 것이다.

마지막으로 새 예측기가 업데이트된 가중치를 사용해 훈련되고 전체 과정이 반복된다. 새 예측기의 가중치가 계산되고 샘플의 가중치를 업데이트해서 또 다른 예측기를 훈련시키는 식이다. 이 알고리즘은 지정된 예측기 수에 도달하거나 완벽한 예측기가 만들어지면 중지된다.

예측을 할 때 아다부스트는 단순히 모든 예측기의 예측을 계산하고 예측기 가중치 $\alpha_j$를 더해 예측 결과를 만든다. 가중치 합이 가장 큰 클래스가 예측 결과가 된다.
$\hat{y}(x) = \underset{k}{argmax} \sum_{j=1}^N \alpha_j$ 여기서 $\hat{y}_j(x)=k$, N은 예측기 수

사이킷런에선 AdaBoostClassifier 를 사용한다. 사이킷런은 SAMME 라는 아다부스트의 다중 클래스 버전을 사용하는데, 클래스가 2개일 때는 아다부스트와 동일하다. 예측기가 클래스의 확률을 추정할 수 있다면 사이킷런은 SAMME.R 이라는 변종을 사용한다. 이 알고리즘은 예측값 대신 클래스 확률에 기반하여 일반적으로 성능이 더 좋다.

from sklearn.ensemble import AdaBoostClassifier

ada_clf = AdaBoostClassifier(
    DecisionTreeClassifier(max_depth=1), n_estimators=200,
    algorithm="SAMME.R", learning_rate=0.5, random_state=42)
ada_clf.fit(X_train, y_train)

7.5.2 그래디언트 부스팅

또 다른 부스팅 알고리즘은 그래디언트 부스팅으로, 반복마다 샘플의 가중치를 수정하는 아다부스트와 달리 이전 예측기가 만든 잔여 오차(residual error)에 새로운 예측기를 학습시킨다.

from sklearn.tree import DecisionTreeRegressor

tree_reg1 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg1.fit(X, y)

첫 번째 예측기에서 생긴 잔여 오차에 두 번째 DecisionTreeRegressor 를 훈련시킨다.

y2 = y - tree_reg1.predict(X)
tree_reg2 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg2.fit(X, y2)

그런 다음 두 번째 예측기가 만든 잔여 오차에 세 번째 회귀 모델을 훈련시킨다.

y3 = y2 - tree_reg2.predict(X)
tree_reg3 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg3.fit(X, y3)

이렇게 3개의 트리가 생겼고, 새로운 샘플에 대한 예측을 만드려면 모든 트리의 예측을 더하면 된다.

y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))

아래 그림에서 왼쪽 열은 각 트리의 예측이고, 오른쪽 열은 앙상블의 예측이다. 트리가 합해질수록 모델이 더 정확하게 예측하는 것(오른쪽 열 맨 아래 그림)을 확인할 수 있다.

사이킷런에선 GradientBoostingRegressor 를 사용하면 앙상블을 간단하게 훈련시킬 수 있다.

from sklearn.ensemble import GradientBoostingRegressor

gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0, random_state=42)
gbrt.fit(X, y)
learning_rate: 각 트리의 기여 정도 조절(축소=shrinkage라고 부르는 규제 방법)
- 낮게 설정하면 많은 트리가 필요하지만 일반적으로 예측 성능은 좋아진다. 다만 오버피팅의 위험이 있다.

최적의 트리 수를 찾기 위해 조기 종료 기법을 사용할 수 있는데, 간단히 구현하려면 staged_predcit() 메서드를 사용한다. 이 메서드는 훈련의 각 단계에서 앙상블에 의해 만들어진 예측기를 순회하는 반복자(iterator)를 반환한다.

import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=49)

gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=120, random_state=42)
gbrt.fit(X_train, y_train)

errors = [mean_squared_error(y_val, y_pred)
          for y_pred in gbrt.staged_predict(X_val)]
bst_n_estimators = np.argmin(errors) + 1

gbrt_best = GradientBoostingRegressor(max_depth=2, n_estimators=bst_n_estimators, random_state=42)
gbrt_best.fit(X_train, y_train)

위처럼 최적의 수를 찾는 과정을 거치는 대신 실제로 훈련을 중지하는 방법으로 조기 종료를 구현할 수도 있다. warm_start=True 로 설정하면 사이킷런이 fit() 메서드가 호출될 때 기존 트리를 유지하고 훈련을 추가하게 된다.

gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True, random_state=42)

min_val_error = float("inf")
error_going_up = 0
for n_estimators in range(1, 120):
    gbrt.n_estimators = n_estimators
    gbrt.fit(X_train, y_train)
    y_pred = gbrt.predict(X_val)
    val_error = mean_squared_error(y_val, y_pred)
    if val_error < min_val_error:
        min_val_error = val_error
        error_going_up = 0
    else:
        error_going_up += 1
        if error_going_up == 5:
            break  # early stopping
(참고)
GradientBoostingRegressor 는 각 트리가 훈련할 때 사용할 훈련 샘플의 비율을 지정할 수 있는 subsample 매개변수도 지원한다. 예를 들어 subsample=0.25 라고 하면 각 트리는 무작위로 선택된 25%의 훈련 샘플로 학습된다. 편향이 높아지는 대신 분산은 낮아지게 되며, 이 방법을 확률적 그래디언트 부스팅(Stochastic Gradient Boosting)이라 한다.

7.6 스태킹

스태킹은 앙상블에 속한 모든 예측기의 예측을 취합하는 간단한 함수(직접 투표 등)를 사용하는 대신 취합하는 모델을 훈련하는 것이다. 아래 그림을 보면 서로 다른 3개의 예측기가 회귀 예측을 하고 마지막 예측기(blender or meta learner)가 이 예측을 입력으로 받아 최종 예측을 만든다.

블렌더를 학습시키는 일반적인 방법은 홀드 아웃(hold-out) 세트를 사용하는 것이다. 먼저 훈련 세트를 2개의 서브셋으로 나누는데, 첫 번째 서브셋은 첫 번째 레이어의 예측을 훈련시키기 위해 사용된다. 

그런 다음 첫 번째 레이어의 예측기를 사용해 두 번째(홀드 아웃) 세트에 대한 예측을 만든다. 타깃값은 그대로 쓰고 앞에서 예측한 값을 입력 특성으로 사용하는 새로운 훈련 세트를 만들 수 있다. 결국 첫 번째 레이어의 예측을 가지고 타깃값을 예측하도록 학습되는 방식이다.

블렌더를 여러 개 훈련시키는 것도 가능하다. 훈련 세트를 n개의 서브셋으로 나누면 n개 레이어가 생성된다. 

첫 번째 세트는 첫 번째 레이어를 훈련시키는 데 사용되고,
두 번째 세트는 (첫 번째 레이어의 예측기로) 두 번째 레이어를 훈련시키기 위한 훈련 세트를 만드는 데 사용된다.
세 번째 세트는 (두 번째 레이어의 예측기로) 세 번째 레이어를 훈련시키기 위한 훈련 세트를 만드는 데 사용된다.
사이킷런은 스태킹을 지원하지 않기 때문에 직접 구현해야 한다.

728x90
반응형