본문 바로가기
Python (Linux)

45. Python - 앙상블 학습과 랜덤포레스트

by #Glacier 2019. 2. 21.
반응형

안녕하세요. 오늘은 드디어 앙상블 학습과 랜덤포레스트입니다.

제가 기다리고 기다리던 챕터였어요ㅋㅋ

저자가 어떤 것을 설명할지 궁금합니다.


무작위로 선택된 수천 명의 사람의 의견이 전문가의 답보다 낫다. 이를 대중의 지혜, Wisdom of the crowd 라고 합니다.

즉 이와 비슷하게, 일련의 예측기(분류나 회귀 모델)로부터 예측을 수집하면, 

가장 좋은 모델 하나보다 더 좋은 예측을 얻을 수 있습니다. 

일련의 예측기를 앙상블이라고 부르기 때문에 이를 앙상블 학습이라고 하며,

앙상블 학습 알고리즘을 앙상블 기법, 혹은 방법이라 부릅니다.


예를 들어, 훈련 세트로부터 무작위로 각기 다른 서브셋을 만들어 일련의 결정 트리 분류기를 훈련시킵니다.

그리고, 모든 개별 트리의 예측을 구하면 됩니다. 그런 다음, 가장 많은 선택을 받은 클래스를 예측으로 삼습니다.

결정 트리의 앙상블을 랜덤 포레스트(Random Forest)라고 합니다. 

간단한 방법임에도, 랜덤 포레스트는 오늘날 가장 강력한 머신러닝 알고리즘 중 하나입니다.


프로젝트의 마지막에 다다르면, 흔히 앙상블 기법을 사용하여 이미 만든 여러 괜찮은 예측기를 연결하여 더 좋은 예측기를 만듭니다. 사실 머신러닝 경연 대회에서 우승하는 솔루션은 여러 가지 앙상블 방법을 사용한 경우가 많다고 하네요.


이 장에서는 배깅, 부스팅, 스태킹 등 가장 인기 있는 앙상블 방법을 설명하고, 랜덤 포레스트도 다룹니다.


1. 투표 기반 분류기


정확도가 80%인 분류기를 여러 개 훈련시켰다고 가정합시다. 아마도 로지스틱 회귀 분류기, SVM 분류기, 랜덤 포레스트 분류기, K-최근접 이웃 분류기 등을 가지고 있을 수 있습니다. 

더 좋은 분류기를 만드는 매우 간단한 방법은, 각 분류기의 예측을 모아서, 가장 많이 선택된 클래스를 예측하는 것입니다.

이렇게 다수결 투표로 정해지는 분류기를 직접 투표 (hard voting) 분류기라고 합니다.

각 분류기가 약한 학습기(weak learner) 즉 랜덤 추측보다 조금 더 높은 성능인 분류기일지라도 충분하게 많고 다양하다면 앙상블은 높은 정확도를 갖는 강한 학습기가 될 수 있습니다.


이게 어떻게 가능하냐면, 대수의 법칙(low of large numbers) 때문입니다. 

동전을 던졌을 때, 앞면이 51% 뒷면이 49%가 나오는 조금 균형이 맞지 않는 동전이 있다고 가정하면,

1,000번 던졌을 경우 앞면이 510번, 뒷면이 490번 나올 것입니다.

수학적으로 1,000번을 던진 후 앞면이 다수가 될 확률은 75%에 가깝고, 더 많이 던질수록 확률은 증가합니다.

아래의 그림은 균형이 틀어진 동전을 10번 실험한 그래프입니다. 

던진 횟수가 증가할수록 앞면이 나올 확률 51%에 가까워집니다. 

결국, 10번의 실험 모두, 50%보다 높게 유지되며 51%에 수렴하면서 끝나는 것을 볼 수 있습니다.


#기본 설정

import numpy as np

import os


import matplotlib

import matplotlib.pyplot as plt


plt.rcParams['axes.labelsize'] = 14

plt.rcParams['xtick.labelsize'] = 12

plt.rcParams['ytick.labelsize'] = 12


#앞면 51% 뒷면 49% 의 동전 던지기 10번 실험


heads_proba = 0.51

coin_tosses = (np.random.rand(10000,10) < heads_proba).astype(np.int32)

cumulative_heads_ratio = np.cumsum(coin_tosses, axis=0) / np.arange(1, 10001).reshape(-1, 1)


plt.figure(figsize=(8,3.5))

plt.plot(cumulative_heads_ratio)

plt.plot([0, 10000], [0.51, 0.51], 'k--', linewidth=2, label='51%')

plt.plot([0, 10000], [0.5, 0.5], 'k-', label='50%')

plt.xlabel('동전을 던진 횟수')

plt.ylabel('앞면이 나온 비율')

plt.legend(loc='lower right')

plt.axis([0, 10000, 0.42, 0.58])

plt.show()



이처럼, 무작위 추측보다 조금 더 나은 51% 정확도를 가진 1,000개의 분류기로 앙상블 모델을 구축한다고 가정합시다.

가장 많은 클래스를 예측으로 삼는다면 75%의 정확도를 기대할 수 있습니다.

하지만, 이런 가정은 모든 분류기가 완벽하게 독립적이며, 오차의 상관관계가 없어야 합니다.

하지만 여기서는 같은 데이터로 훈련시키기 때문에 가정이 맞지 않고, 분류기들이 같은 종류의 오차를 만들기 쉽기 때문에 잘못된 클래스가 다수인 경우가 많고 앙상블의 정확도가 낮아집니다.


[TIP]

앙상블 방법은, 예측기가 가능한 서로 독립적일 때 최고의 성능을 발휘합니다.

다양한 분류기를 얻는 한 가지 방법은 각기 다른 알고리즘으로 학습시키는 것입니다.

이렇게 하면 매우 다른 종류의 오차를 만들 가능성이 높기 때문에 앙상블 모델의 정확도를 향상시킵니다.


아래는 여러 분류기를 조합하여, 사이킷 런의 투표 기반 분류기(VotingClassifier)를 만들고 훈련시키는 코드입니다.

훈련 세트는 moons 데이터셋입니다.


from sklearn.model_selection import train_test_split

from sklearn.datasets import make_moons


X, y = make_moons(n_samples =500, noise=0.30, random_state=42)

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)


#사이킷런 0.22 버전에서 LogisticRegression의 solver 매개변수의 기본값이 'liblinear'에서

#'lbfgs'로 변경될 예정입니다. 책의 결과와 맞추기 위해 명시적으로 'liblinear'로 설정합니다. 

#0.22 버전에서 RandomForestClassifier의 n_estimators 매개변수 기본값이 100으로 바뀝니다.

#경고를 내지 않도록 하기 위해 현재 기본값인 10으로 설정합니다. 

#0.22 버전에서 SVC의 gamma 매개변수의 기본값이 스케일 조정되지 않은 특성을 위해 

#'scale'로 변경됩니다. 기존 방식을 사용하고 경고를 없애기 위해 'auto'로 설정합니다.


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='liblinear', random_state=42)

rnd_clf = RandomForestClassifier(n_estimators=10, random_state=42)

svm_clf = SVC(gamma='auto', 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)

VotingClassifier(estimators=[('lr', LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='warn',
          n_jobs=None, penalty='l2', random_state=42, solver='liblinear',
          tol=0.0001, verbose=0, warm_start=False)), ('rf', Rando...f',
  max_iter=-1, probability=False, random_state=42, shrinking=True,
  tol=0.001, verbose=False))],
         flatten_transform=None, n_jobs=None, voting='hard', weights=None)


#각 분류기의 테스트셋 정확도 확인


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))

LogisticRegression 0.864
RandomForestClassifier 0.872
SVC 0.888
VotingClassifier 0.896


예상대로, 투표 기반 분류기가 다른 개별 분류기보다 성능이 조금 더 높음을 알 수 있습니다.

모든 분류기가 클래스의 확률을 예측할 수 있으면, (즉, predict_proba() 메서드가 있으면) 

개별 분류기의 예측을 평균내어 확률이 가장 높은 클래스를 예측할 수 있습니다. 이를 간접 투표(soft voting) 라고 합니다.

이 방식은 확률이 높은 투표에 비중을 더 두기 때문에 직접 투표보다 성능이 높습니다.

이 방식을 사용하기 위해서는 voting='soft' 로 바꾸고, 모든 분류기가 클래스의 확률을 예측할 수 있으면 됩니다.

SVC는 기본값에서 클래스 확률을 제공하지 않으므로 probability 매개변수를 True로 지정해야 합니다.
(이렇게 하면 클래스 확률을 추정하기 위해 교차 검증을 사용하므로 훈련 속도가 느려지지만 SVC에서 predict_proba()메서드를 사용할 수 있습니다.)

이처럼 한다면 약 91%정도의 정확도를 얻을 수 있을 것입니다.


log_clf = LogisticRegression(solver='liblinear', random_state=42)

rnd_clf = RandomForestClassifier(n_estimators=10, random_state=42)

svm_clf = SVC(gamma='auto', probability=True, random_state=42)


voting_clf = VotingClassifier(

                                                            estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)],

                                                            voting='soft')

voting_clf.fit(X_train, y_train)

VotingClassifier(estimators=[('lr', LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='warn',
          n_jobs=None, penalty='l2', random_state=42, solver='liblinear',
          tol=0.0001, verbose=0, warm_start=False)), ('rf', Rando...bf',
  max_iter=-1, probability=True, random_state=42, shrinking=True,
  tol=0.001, verbose=False))],
         flatten_transform=None, n_jobs=None, voting='soft', weights=None)

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))

LogisticRegression 0.864
RandomForestClassifier 0.872
SVC 0.888
VotingClassifier 0.912



2. 배깅과 페이스팅


다양한 분류기를 만드는 한 가지 방법은 각기 다른 훈련 알고리즘을 사용하는 것이라고 앞서 언급했습니다.

또 다른 방법은 같은 알고리즘을 사용하지만 훈련 세트의 서브셋을 무작위로 구성하여 분류기를 각기 다르게 학습시키는 것입니다.


훈련 세트에서 중복을 허용하여 샘플링 하는 방식을 배깅(bagging, bootstrap aggregating의 줄임말)이라 하며,

중복을 허용하지 않고 샘플링하는 방식을 페이스팅(pasting)이라고 합니다.

통계학에서는 중복을 허용한 리샘플링을 부트스트래핑 이라고 합니다. (예를 들면.. 잔차 부트스트래핑)


다시 말해 배깅과 페이스팅에서는 같은 훈련 샘플을 여러 개의 예측기에 걸쳐 사용할 수 있습니다.

하지만, 배깅만이 한 예측기를 위해 훈련 샘플을 여러 번 샘플링 할 수 있습니다. 


모든 예측기가 훈련을 마치면, 앙상블은 모든 예측기의 예측을 모아서 새로운 샘플에 대한 예측을 만듭니다.

수집 함수는 전형적으로 분류일 때는 통계적 최빈값(statistical mode, 직접 투표 분류기처럼 가장 많은 예측 결과), 이고

회귀에 대해서는 평균을 계산합니다.


개별 예측기는 원본 훈련 세트로 훈련시킨 것 보다 훨씬 크게 편향되어 있지만, 수집 함수를 통과하면 편향과 분산이 모두 감소합니다. 일반적으로 앙상블의 결과는 원본 데이터셋으로 하나의 예측기를 훈련시킬 때와 비교해 편향은 비슷하지만, 분산은 줄어드는 효과를 얻습니다.

(이전에 편향과 분산의 Trade-off 관계가 있다고 했었죠!, 이를 조금 무시하게 되는 것이군요..)


#편향이란 이미 소개한 바 있지만, 일반화 오차 중에서 잘못된 가정으로 인한 것입니다.

예를 들어 실제로 데이터가 2차인데 선형으로 가정하는 경우입니다. 편향이 큰 모델은 훈련 데이터에 과소적합되기 쉽습니다.


#분산이란 훈련 데이터에 있는 작은 변동에 모델이 과도하게 민감하기 때문에 나타납니다.

자유도가 높은 모델(고차 다항 회귀모델과 같은)이 높은 분산을 가지기 쉬워 훈련 데이터에 과대적합되는 경향이 있습니다.


예측기는 모두 동시에 다른 CPU코어나 서버에서 병렬로 학습시킬 수 있습니다. 

이와 유사하게 예측도 병렬로 수행할 수 있습니다. 이런 확장성 때문에 배깅과 페이스팅의 인기가 높습니다.


사이킷런은 배깅과 페이스팅을 위해 간편한 API로 구성된 BaggingClassifier(회귀의 경우 BaggingRegressor)를 제공합니다.

다음은 결정 트리 분류기 500개의 앙상블을 훈련시키는 코드입니다. 각 분류기는 훈련 세트에서 중복을 허용하여 무작위로 선택된 100개의 샘플로 훈련됩니다. (이는 배깅의 경우이고, 페이스팅을 사용하려면 bootstrap=False로 지정)


from sklearn.ensemble import BaggingClassifier

from sklearn.tree import DecisionTreeClassifier


bag_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500, max_samples=100, 

                                                           bootstrap=True, n_jobs=-1)


bag_clf.fit(X_train, y_train)

y_pred = bag_clf.predict(X_test)


from sklearn.metrics import accuracy_score


print(accuracy_score(y_test, y_pred))

0.92


tree_clf = DecisionTreeClassifier(random_state=42)

tree_clf.fit(X_train, y_train)

y_pred_tree = tree_clf.predict(X_test)

print(accuracy_score(y_test, y_pred_tree))

0.856


*max_samples 매개변수는 0.0~1.0 사이의 실수로 지정할 수도 있습니다. 

이렇게 지정하면 샘플링되는 데이터 수는 훈련 세트의 크기에 max_samples를 곱한 값이 됩니다.


*BaggingClassifier는 기반이 되는 분류기가 결정 트리 분류기처럼 클래스 확률을 추정할 수 있으면

 (predict_proba()메서드가 있으면) 직접 투표 대신 자동으로 간접 투표 방식을 사용합니다.


아래의 그림은 단일 결정 트리의 결정 경계와 500개의 트리를 사용한 배깅 앙상블(위의 모델)의 결정 경계를 비교한 것입니다.

둘 다 moons 데이터셋에 훈련시켰습니다. 여기서 볼 수 있듯이, 앙상블의 예측이 결정 트리 하나의 예측보다 일반화가 훨씬 잘 된 것 같습니다. 앙상블은 비슷한 편향에서 더 작은 분산을 만드므로 훈련 세트의 오차 수가 거의 비슷하지만 결정 경계는 덜 불규칙하게 됩니다.


from matplotlib.colors import ListedColormap


def plot_decision_boundary(clf, X, y, axes=[-1.5, 2.5, -1, 1.5], alpha=0.5, contour=True):

    x1s = np.linspace(axes[0], axes[1], 100)

    x2s = np.linspace(axes[2], axes[3], 100)

    x1, x2 = np.meshgrid(x1s, x2s)

    X_new = np.c_[x1.ravel(), x2.ravel()]

    y_pred = clf.predict(X_new).reshape(x1.shape)

    custom_cmap = ListedColormap(['#fafab0', '#9898ff', '#a0faa0'])

    plt.contourf(x1, x2, y_pred, alpha=0.3, cmap=custom_cmap)

    if contour :

        custom_cmap2 = ListedColormap(['#7d7d58', '#4c4c7f', '#507d50'])

        plt.contour(x1, x2, y_pred, cmap=custom_cmap2, alpha=0.8)

    plt.plot(X[: ,0][y==0], X[:, 1][y==0], 'yo', alpha=alpha)

    plt.plot(X[:, 0][y==1], X[:, 1][y==1], 'bs', alpha=alpha)

    plt.axis(axes)

    plt.xlabel(r'$x_1$', fontsize=18)

    plt.ylabel(r'$x_2$', fontsize=18, rotation=0)


plt.figure(figsize=(11,4))

plt.subplot(121)

plot_decision_boundary(tree_clf, X, y)

plt.title('결정 트리', fontsize=14)


plt.subplot(122)

plot_decision_boundary(bag_clf, X, y)

plt.title('배깅을 사용한 결정 트리', fontsize=14)

plt.show()



부트스트래핑은 각 예측기가 학습하는 서브셋에 다양성을 증가시키므로, 배깅이 페이스팅보다 편향이 조금 더 높습니다.

하지만 이는 예측기들의 상관관계를 줄이므로, 앙상블의 분산을 감소시킵니다. 전반적으로 배깅이 더 나은 모델을 만들기 때문에 선호합니다. 하지만 CPU, 시간에 여유가 있다면 교차 검증으로 배깅과 페이스팅을 모두 평가해서 더 나은 쪽을 선택하는 것이 좋습니다.


3. oob 평가


배깅을 사용하면 어떤 샘플은 한 예측기를 위해 여러 번 샘플링 되고, 어떤 것은 전혀 선택되지 않을 수 있습니다.

BaggingClassifier는 기본값으로 중복을 허용하여 (bootstrap=True) 훈련 세트의 크기만큼인 m개 샘플을 선택합니다.

이는 평균적으로 각 예측기에 훈련 샘플의 63%정도만 샘플링된다는 것을 의미합니다.

선택되지 않은 훈련 샘플의 나머지 37%를 oob (out-of-bag)  샘플이라고 부릅니다.

예측기마다 남겨진 37%는 모두 다릅니다.


** 훈련 세트의 크기인 m이 커지면 이 비율이 1-exp(-1) , 63.212%에 가까워집니다. 

m개의 샘플에서 무작위로 하나를 추출할 때 선택되지 않을 확률 : 

이를 m번 반복했을 때도 선택되지 않았을 확률은 과 같습니다. 

여기에 로그를 취하고, 로피탈의 정리를 이용하면 이 값이 과 같아집니다.

여기서 1-=0.63212가 됩니다.


y = \left(1 - \dfrac{1}{n}\right)^n

수학적 트릭을 위해 양변에 로그를 취합니다.

ln(y) = ln \left( 1 - \dfrac{1}{n}\right)^n = n\,ln \left( 1 - \dfrac{1}{n}\right)

그런 다음 새로운 변수 x = \dfrac {1}{n}를 적용합니다.

= \dfrac {1}{x} ln (1 - x) = \dfrac {ln (1 - x)}{x}

n \to \infty, 즉 x \to 0일 때 로피탈의 정리를 적용하면

ln(y) = \underset{x \to 0}{\lim} \dfrac {ln (1 - x)}{x} = \underset{x \to 0}{\lim} \dfrac {\frac{d}{dx}ln (1 - x)}{\frac{d}{dx}x} = \underset{x \to 0}{\lim} \dfrac{-1}{1-x} = -1

이 됩니다. 이제 로그를 풀고 y에 대해 정리하면

y = e^{-1} = 0.3678794...

이 됩니다. 즉 샘플 개수 n이 아주 클 때, 어떤 샘플이 무작위한 n번의 선택에 한번도 포함되지 않을 확률은 36.8%정도 입니다. 그리고 샘플 개수가 아주 작을 때 가령 n=3이면

y = \left(1-\dfrac{1}{3}\right)^3=0.296296...

[ 출처 : 옮긴이의 블로그 https://goo.gl/ifFbg3 ]



예측기가 훈련되는 동안에는 oob 샘플을 사용하지 않으므로, 검증 세트나 교차 검증을 사용하지 않고 oob 샘플을 사용해 평가할 수 있습니다. 앙상블의 평가는 각 예측기의 oob평가를 평균하여 얻습니다.


사이킷런에서 BaggingClassifier를 만들 때 oob_score=True로 지정하면 훈련이 끝난 후 자동으로 oob 평가를 수행합니다.

다음 코드는 이 과정을 보여줍니다. 평가 점수는 oob_score_ 변수에 저장되어 있습니다.

bag_clf = BaggingClassifier(

                                                         DecisionTreeClassifier(random_state=42), n_estimators=500,

                                                         bootstrap=True, n_jobs=-1, oob_score=True, random_state=40)

bag_clf.fit(X_train, y_train)

bag_clf.oob_score_

0.9013333333333333

# oob 평가 결과를 보면, 이 BaggingClassifier는 테스트 세트에서 약 90.1%정도의 정확도를 얻을 것으로 보입니다.

# 확인


from sklearn.metrics import accuracy_score

y_pred = bag_clf.predict(X_test)

accuracy_score(y_test, y_pred)

0.912

#매우 비슷한 결과를 얻었습니다.


#oob 샘플에 대한 결정 함수의 값고 oob_decision_function_ 변수에서 확인할 수 있습니다.

#이 경우 결정 함수는 각 훈련 샘플의 클래스 확률을 반환합니다. (기반이 되는 예측기가 predict_proba() 메서드를 가지고 있음)

#아래의 예를 보면, 첫 훈련 샘플이 양성 클래스에 속할 확률을 68.25%로 추정하고 있습니다.


bag_clf.oob_decision_function_

array([[0.31746032, 0.68253968], [0.34117647, 0.65882353], [1. , 0. ], [0. , 1. ], [0. , 1. ], [0.08379888, 0.91620112], [0.31693989, 0.68306011]])

(이하 생략)


4. 랜덤 패치와 랜덤 서브스페이스


BaggingClassifier는 특성 샘플링도 지원합니다. max_features, bootstrap_features 두 매개변수로 조절할 수 있습니다.

작동 방식은 max_samples, bootstrap과 동일하지만 샘플이 아닌 특성에 대한 샘플링입니다. 


따라서, 각 예측기는 무작위로 선택한 입력 특성의 일부분으로 훈련됩니다.

이는 특히 이미지와 같은 매우 고차원의 데이터셋을 다룰 때 유용합니다.

훈련 샘플과 특성을 모두 샘플링하는 것을 랜덤 패치 방식(Random Patches Method) 이라고 합니다.

훈련 샘플을 모두 사용하고(즉 bootstrap=False, max_sameples=1.0) 특성을 샘플링하는 (bootstrap_features=True, max_features는 1.0보다 작은) 것을 랜덤 서브스페이스(Random Subspace Method)라고 합니다.


특성 샘플링은 더 다양한 예측기를 만들며 편향을 늘리는 대신 분산을 낮춥니다.


5. 랜덤 포레스트


앞서 언급했듯 랜덤 포레스트는 일반적으로 배깅 기법(또는 페이스팅)을 적용한 결정 트리의 앙상블입니다.

전형적으로 max_samples를 훈련 세트의 크기로 지정합니다.

BaggingClassifier에 DecisionTreeClassifier를 넣어 만드는 대신, 결정 트리에 최적화되어 사용하기 편리한 RandomForestClassifier를 사용할 수 있습니다. 아래는 16개의 리프 노드를 갖는 500개의 트리로 이루어진 랜덤 포레스트 분류기를 여러 CPU 코어에서 훈련시키는 코드입니다.


bag_clf = BaggingClassifier(

      DecisionTreeClassifier(splitter='random', max_leaf_nodes=16, random_state=42),

      n_estimators=500, max_samples=1.0, bootstrap=True, n_jobs= -1, random_state=42)


bag_clf.fit(X_train, y_train)

y_pred = bag_clf.predict(X_test)



from sklearn.ensemble import RandomForestClassifier


rnd_clf = RandomForestClassifier(n_estimators=500, max_leaf_nodes=16, n_jobs= -1, random_state=42)

rnd_clf.fit(X_train, y_train)


y_pred_rf  = rnd_clf.predict(X_test)


np.sum(y_pred == y_pred_rf) / len(y_pred) #거의 동일한 예측

0.976



RandomForestClassifier는 몇 가지 예외가 있지만, 트리의 성장을 조절하기 위한 DecisionTreeClassifier의 매개변수와 앙상블 자체를 제어하는 데 필요한 BaggingClassifier의 매개변수를 모두 가지고 있습니다.


랜덤 포레스트 알고리즘은 트리의 노드를 분할할 때 전체 특성 중에서 최선의 특성을 찾는 대신, 무작위로 선택한 특성 후보 중에서 최적의 특성을 찾는 식으로 무작위성을 더 주입합니다. 이는 결국, 트리를 더욱 다양하게 만들고, 분산을 낮추어 더 훌륭한 모델을 만들어 냅니다. 


6. 엑스트라 트리


랜덤 포레스트에서 트리를 만들 때, 각 노드는 무작위로 특성의 서브셋을 만들어 분할에 사용합니다.

트리를 더욱 무작위하게 만들기 위해 (보통의 결정 트리처럼) 최적의 임곗값을 찾는 대신 후보 특성을 사용해 무작위로 분할한 다음, 그 중에서 최상의 분할을 선택합니다.


이와 같이 극단적으로 무작위한 트리의 랜덤 포레스트를 익스트림 랜덤 트리(Extremely Randomized Trees) 앙상블 (또는 줄여서 엑스트라 트리, Extra-Trees) 이라고 부릅니다.

여기서도 역시 편향이 늘어나지만, 분산이 줄어들게 됩니다. 모든 노드에서 특성마다 가장 최적의 임곗값을 찾는 것이 트리 알고리즘에서 가장 시간이 많이 소요되는 작업 중 하나이므로 일반적인 랜덤 포레스트보다 엑스트라 트리가 훨씬 빠릅니다.


엑스트라 트리를 만들려면 사이킷런의 ExtraTreeClassifier를 사용합니다. 사용법은 RandomForestClassifier와 같습니다.

마찬가지로, ExtraTreeRegressor도 RandomForestRegressor와 같은 API를 제공합니다.


7. 특성 중요도


랜덤 포레스트의 또 다른 장점은 특성의 상대적 중요도를 측정하기 쉽다는 것입니다.

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


사이킷런은 훈련이 끝난 뒤 특성마다 자동으로 이 점수를 계산하고, 중요도의 전체 합이 1이 되도록 결괏값을 정규화합니다.

이 값은 feature_importances 변수에 저장되어 있습니다.


from sklearn.datasets import load_iris

iris = load_iris()

rnd_clf = RandomForestClassifier(n_estimators=500, n_jobs = -1, random_state=42)

rnd_clf.fit(iris['data'], iris['target'])


for name, score in zip(iris['feature_names'], rnd_clf.feature_importances_):

    print(name, score)

sepal length (cm) 0.11249225099876374
sepal width (cm) 0.023119288282510326
petal length (cm) 0.44103046436395765
petal width (cm) 0.4233579963547681

rnd_clf.feature_importances_

array([0.11249225, 0.02311929, 0.44103046, 0.423358  ])


plt.figure(figsize=(6, 4))


for i in range(15):

    tree_clf = DecisionTreeClassifier(max_leaf_nodes = 16, random_state=42 + i)

    indices_with_replacement = np.random.randint(0, len(X_train), len(X_train))

    tree_clf.fit(X[indices_with_replacement], y[indices_with_replacement])

    plot_decision_boundary(tree_clf, X, y, axes=[-1.5, 2.5, -1, 1.5], alpha=0.02, contour=False)

    

plt.show()



#이와 유사하게 MNIST 데이터셋으로 랜덤 포레스트 분류기를 훈련시키고, 각 픽셀의 중요도를 그래프로 나타내면 

#아래와 같은 그림을 얻을 수 있습니다.


from sklearn.datasets import fetch_openml

mnist = fetch_openml('mnist_784', version=1)


rnd_clf = RandomForestClassifier(n_estimators=10, random_state=42)

rnd_clf.fit(mnist['data'], mnist['target'])

RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
            max_depth=None, max_features='auto', max_leaf_nodes=None,
            min_impurity_decrease=0.0, min_impurity_split=None,
            min_samples_leaf=1, min_samples_split=2,
            min_weight_fraction_leaf=0.0, n_estimators=10, n_jobs=None,
            oob_score=False, random_state=42, verbose=0, warm_start=False)

def plot_digit(data):

    image = data.reshape(28, 28)

    plt.imshow(image, cmap=matplotlib.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(['중요하지 않음', '매우 중요함'])


plt.show()



** 결정 트리를 기반으로 하는 모델은 모두 특성 중요도를 제공합니다.

** DecisionTreeClassifier의 특성 중요도는 일부 특성을 완전히 배제시키지만, 무작위성이 주입된 RandomForestClassifier는 거의 모든 특성에 대해 평가할 기회를 가집니다.


8. 부스팅


부스팅(boosting) (원래는 가설 부스팅, hypothesis boosting 이라 불렀습니다.) 은 약한 학습기를 여러 개 연결하여 강한 학습기를 만드는 앙상블 방법을 말합니다

부스팅 방법의 아이디어는 앞의 모델을 보완해나가면서 일련의 예측기를 학습시키는 것입니다. 

부스팅 방법 중 가장 인기있는 것은 아다부스트(AdaBoost, Adaptive Boosting의 줄임말)와 그래디언트 부스팅(Gradient Boosting)입니다. 


1. AdaBoost


이전 예측기를 보완하는 새로운 예측기를 만드는 방법은 이전 모델이 과소적합했던 훈련 샘플의 가중치를 더 높히는 것입니다.

이렇게 하면 새로운 예측기는 학습하기 어려운 샘플에 점점 더 맞춰지게 됩니다. 그리고, 이것이 아다부스트의 방식입니다.


예를 들어, 아다부스트 분류기를 만들려면 기반이 되는 첫 번째 분류기(ex, 결정 트리)를 훈련 세트에서 훈련시키고 예측을 만듭니다. 그 다음에 잘못 분류된 훈련 샘플의 가중치를 상대적으로 높입니다. 두 번째 분류기는 업데이트된 가중치를 사용해 훈련 세트에서 훈련하고 다시 예측을 만듭니다. 이 방법이 반복되는 방식입니다.


사이킷런의 AdaBoostClassifier를 사용하여 200개의 아주 얕은 결정 트리를 기반으로 하는 아다부스트 분류기를 훈련시킵니다.


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)


plot_decision_boundary(ada_clf, X, y)


m = len(X_train)


plt.figure(figsize=(11,4))

for subplot, learning_rate in ((121, 1), (122, 0.5)):

    sample_weights = np.ones(m)

    plt.subplot(subplot)

    if subplot == 121 :

        plt.text(-0.7, -0.65, "1", fontsize=14)

        plt.text(-0.6, -0.10, "2", fontsize=14)

        plt.text(-0.5,  0.10, "3", fontsize=14)

        plt.text(-0.4,  0.55, "4", fontsize=14)

        plt.text(-0.3,  0.90, "5", fontsize=14)        

    for i in range(5):

        svm_clf = SVC(kernel="rbf", C=0.05, gamma='auto', random_state=42)

        svm_clf.fit(X_train, y_train, sample_weight=sample_weights)

        y_pred = svm_clf.predict(X_train)

        sample_weights[y_pred != y_train] *= (1 + learning_rate)

        plot_decision_boundary(svm_clf, X, y, alpha=0.2)

        plt.title("learning_rate = {}".format(learning_rate), fontsize=16)


plt.show()


위의 코드는 moons 데이터셋에 훈련시킨 다섯 개의 연속된 예측기의 결정 경계입니다.

(이 모델은 규제를 강하게 한 RBF 커널 SVM 분류기입니다.)


첫 번째 분류기가 많은 샘플을 잘못 분류해서, 샘플들의 가중치가 높아졌습니다.

따라서 두 번째 분류기는 이 샘플들을 더 정확히 예측하게 됩니다.

오른쪽 그래프는 학습률을 반으로 낮춘 것만 빼고 똑같은 예측기를 나타낸 것입니다.

즉, 잘못 분류된 샘플의 가중치는 반복마다 절반 정도만 높아집니다.


(이 그래프는 SVC 모델에서 fit 메서드를 호출할 때, sample_weight 매개변수를 사용해 훈련 샘플의 가중치를 부여하여 만들었습니다. 왼쪽 그래프는 잘못 분류된 샘플의 가중치를 2의 배수로 증가시킨 것이고, 오른쪽 그래프는 1.5배씩 증가시킨 것입니다.)


그림처럼, 이러한 연속된 학습 기법은 경사 하강법과 비슷한 면이 있습니다.

경사 하강법은 비용 함수를 최소화하기 위해 한 에측기의 모델 파라미터를 재조정해가는 반면,

아다부스트는 점차 더 좋아지도록 앙상블에 예측기를 추가합니다.


모든 예측기가 훈련을 마치면, 이 앙상블은 배깅이나 페이스팅과 비슷한 방식으로 예측을 만듭니다.

하지만 가중치가 적용된 훈련 세트의 전반적인 정확도에 따라 예측기마다 다른 가중치가 적용됩니다.


**CAUTION

연속된 학습 기법에는 중요한 단점이 하나 있습니다. 각 예측기는 이전 예측기가 훈련되고 평가된 후에 학습될 수 있기 때문에 병렬화(분할)를 할 수 없습니다. 결국 배깅이나 페이스팅만큼 확장성이 높지는 않습니다.


아다부스트의 알고리즘을 보면, 각 샘플 가중치 는 초기에 로 초기화됩니다.

첫 번째 예측기가 학습되고, 가중치가 적용된 에러율 이 훈련 세트에 대해 계산됩니다.


[ j번째 예측기의 가중치가 적용된 에러율 ]



여기서 는 i번째 샘플에 대한 j번째 예측기의 예측입니다.

예측기의 가중치는 아래의 식을 사용해 계산됩니다. 여기서 (eta)는 학습률 하이퍼파라미터 입니다 (기본값 1).

예측기가 정확할수록, 가중치가 더 높아지게 됩니다. 만약 무작위로 예측하는 정도라면 가중치가 0에 가까울 것입니다.

그러나 그보다 나쁘면, (즉, 무작위 추측보다 정확도가 낮으면, 가중치가 음수가 됩니다.)


[ 예측기 가중치 ]


그 다음은, 아래의 식을 사용해 샘플의 가중치를 업데이트합니다. 즉, 잘못 분류된 샘플의 가중치가 증가됩니다.


[ 가중치 업데이트 규칙 ]


그런 다음, 모든 샘플의 가중치를 정규화합니다. 즉 으로 나눕니다.


마지막으로 새 예측기가 업데이트된 가중치를 사용해 훈련되고, 전체 과정이 반복됩니다.

이 알고리즘은 지정된 예측기의 수에 도달하거나, 완벽한 예측기가 만들어지면 중지됩니다.


예측을 할 때, 아다부스트는 단순히 모든 예측기의 예측을 계산하고, 예측기 가중치 를 더해 예측 결과를 만듭니다.

가중치 합이 가장 큰 클래스가 예측 결과가 됩니다.


[ AdaBoost 예측 ]


여기서, N은 예측기 수입니다.


**원래 아다부스트 알고리즘은 학습률 하이퍼파라미터를 사용하지 않습니다.

**에러율이 0.5(무작위 같은)에 가까워지면 1에 가까워지므로, 예측기 가중치가 0에 가까워집니다.

**에러율이 0.5보다 높으면 예측기 가중치가 음수가 됩니다.


사이킷런은 SAMME라는 아다부스트의 다중 클래스 버전을 사용합니다.

클래스가 두 개 뿐일 때는, SAMME가 아다부스트와 동일합니다. 

예측기가 클래스의 확률을 추정할 수 있다면, (predict_proba() 같은) 사이킷런은 SAMME.R( R은 Real 뜻) 이라는 SAMME의 변종을 사용합니다. 이 알고리즘은 예측값 대신 클래스 확률에 기반하며 일반적으로 성능이 더 좋습니다.

또한, AdaBoostRegressor는 깊이가 3인 DecisionTreeRegressor를 기본 추정기로 사용합니다.


2. 그래디언트 부스팅


인기가 높은 또 하나의 부스팅 알고리즘은 그래디언트 부스팅(Gradient Boosting) 입니다. 

아다부스트처럼 그래디언트 부스팅은 앙상블에 이전까지의 오차를 보정하도록 예측기를 순차적으로 추가합니다.

하지만, 아다부스트처럼 반복마다 샘플의 가중치를 수정하는 대신, 이전 예측기가 만든 잔여 오차(residual error)에 새로운 예측기를 학습시킵니다.


결정 트리를 기반 예측기로 사용하는 간단한 회귀 문제를 풀어보겠습니다.

이를 그래디언트 트리 부스팅(Gradient Tree Boosting) 또는 그래디언트 부스티드 회귀 트리(Gradient Boosted Regression Tree, GBRT) 라고 합니다.


먼저, DecisionTreeRegressor를 훈련 세트(잡음이 섞인 2차 곡선 형태의 훈련 세트)에 학습시켜 보겠습니다.

# Gradient Boosting


np.random.seed(42)

X = np.random.rand(100, 1) - 0.5

y = 3*X[:, 0]**2 + 0.05 * np.random.randn(100)



from sklearn.tree import DecisionTreeRegressor


tree_reg1 = DecisionTreeRegressor(max_depth=2, random_state=42)

tree_reg1.fit(X, y)

DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
           max_leaf_nodes=None, min_impurity_decrease=0.0,
           min_impurity_split=None, min_samples_leaf=1,
           min_samples_split=2, min_weight_fraction_leaf=0.0,
           presort=False, random_state=42, splitter='best')

y2 = y - tree_reg1.predict(X)

tree_reg2 = DecisionTreeRegressor(max_depth=2, random_state=42)

tree_reg2.fit(X, y2)

DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
           max_leaf_nodes=None, min_impurity_decrease=0.0,
           min_impurity_split=None, min_samples_leaf=1,
           min_samples_split=2, min_weight_fraction_leaf=0.0,
           presort=False, random_state=42, splitter='best')

y3 = y2 - tree_reg2.predict(X)

tree_reg3 = DecisionTreeRegressor(max_depth=2, random_state=42)

tree_reg3.fit(X, y3)

DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
           max_leaf_nodes=None, min_impurity_decrease=0.0,
           min_impurity_split=None, min_samples_leaf=1,
           min_samples_split=2, min_weight_fraction_leaf=0.0,
           presort=False, random_state=42, splitter='best')

X_new = np.array([[0.8]])

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

print('y_pred :', y_pred)

y_pred : [0.75026781]


#아래는 과정을 나타낸 것


def plot_predictions(regressors, X, y, axes, label=None, style="r-", data_style="b.", data_label=None):

    x1 = np.linspace(axes[0], axes[1], 500)

    y_pred = sum(regressor.predict(x1.reshape(-1, 1)) for regressor in regressors)

    plt.plot(X[:, 0], y, data_style, label=data_label)

    plt.plot(x1, y_pred, style, linewidth=2, label=label)

    if label or data_label:

        plt.legend(loc="upper center", fontsize=16)

    plt.axis(axes)


plt.figure(figsize=(11,11))


plt.subplot(321)

plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h_1(x_1)$", style="g-", data_label="훈련 세트")

plt.ylabel("$y$", fontsize=16, rotation=0)

plt.title("잔여 오차와 트리의 예측", fontsize=16)


plt.subplot(322)

plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1)$", data_label="훈련 세트")

plt.ylabel("$y$", fontsize=16, rotation=0)

plt.title("앙상블의 예측", fontsize=16)


plt.subplot(323)

plot_predictions([tree_reg2], X, y2, axes=[-0.5, 0.5, -0.5, 0.5], label="$h_2(x_1)$", style="g-", data_style="k+", data_label="잔여 오차")

plt.ylabel("$y - h_1(x_1)$", fontsize=16)


plt.subplot(324)

plot_predictions([tree_reg1, tree_reg2], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1) + h_2(x_1)$")

plt.ylabel("$y$", fontsize=16, rotation=0)


plt.subplot(325)

plot_predictions([tree_reg3], X, y3, axes=[-0.5, 0.5, -0.5, 0.5], label="$h_3(x_1)$", style="g-", data_style="k+")

plt.ylabel("$y - h_1(x_1) - h_2(x_1)$", fontsize=16)

plt.xlabel("$x_1$", fontsize=16)


plt.subplot(326)

plot_predictions([tree_reg1, tree_reg2, tree_reg3], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1) + h_2(x_1) + h_3(x_1)$")

plt.xlabel("$x_1$", fontsize=16)

plt.ylabel("$y$", fontsize=16, rotation=0)


plt.show()



왼쪽 열은 이 세 트리의 예측이고, 오른쪽 열은 앙상블의 예측입니다.

첫 번째 행에서는 앙상블 트리가 하나만 있어서 첫 번째 트리의 예측과 완전히 같습니다.

두 번째 행에서는 새로운 트리가 첫 번째 트리의 잔여 오차에 대해 학습되었습니다.

오른쪽 앙상블 예측이 두 개의 트리 예측의 합과 같은 것을 볼 수 있습니다.

비슷하게, 세 번째 행에서는 또 다른 트리가 두 번째 트리의 잔여 오차에 훈련되었습니다.

트리가 앙상블에 추가될수록 앙상블의 예측이 점점 좋아지는 것을 알 수 있습니다.


사이킷런의 GradientBoostRegressor를 사용하면 GBRT 앙상블을 간단하게 훈련시킬 수 있습니다.

트리 수 (n_estimator)와 같이 앙상블의 훈련을 제어하는 매개변수는 물론 RandomForestRegressor와 아주 비슷하게 결정 트리의 성장을 제어하는 매개변수 (max_depth, min_sample_left 등을 가지고 있습니다.)


** 여기서 보면, 계량경제학 수업에 들었던, 오차 부트스트랩과 매우 유사함을 알 수 있네요.

** residual bootstrap 에서는 실제 y값을 기준으로 회귀분석한 후, 잔차 residual을 y에 더해서 회귀 분석을 계속 진행하는데요,

** 이렇게 진행하게 되면, 이상치에 둔감한 (Robust) 회귀 모델이 만들어집니다.

** 반복하면 할수록 유의한 변수는 coefficient가 증가되고, 유의하지 않은 변수는 coefficient가 감소하게 됩니다.

** 여기서는 Residual을 빼주네요. 


아래는 사이킷런을 사용한 코드입니다.

from sklearn.ensemble import GradientBoostingRegressor


gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=0.1, random_state=42)

gbrt.fit(X, y)

GradientBoostingRegressor(alpha=0.9, criterion='friedman_mse', init=None,
             learning_rate=0.1, loss='ls', max_depth=2, max_features=None,
             max_leaf_nodes=None, min_impurity_decrease=0.0,
             min_impurity_split=None, min_samples_leaf=1,
             min_samples_split=2, min_weight_fraction_leaf=0.0,
             n_estimators=3, n_iter_no_change=None, presort='auto',
             random_state=42, subsample=1.0, tol=0.0001,
             validation_fraction=0.1, verbose=0, warm_start=False)

gbrt_slow = GradientBoostingRegressor(max_depth=2, n_estimators =200, learning_rate=0.1, random_state=42)

gbrt_slow.fit(X, y)

GradientBoostingRegressor(alpha=0.9, criterion='friedman_mse', init=None,
             learning_rate=0.1, loss='ls', max_depth=2, max_features=None,
             max_leaf_nodes=None, min_impurity_decrease=0.0,
             min_impurity_split=None, min_samples_leaf=1,
             min_samples_split=2, min_weight_fraction_leaf=0.0,
             n_estimators=200, n_iter_no_change=None, presort='auto',
             random_state=42, subsample=1.0, tol=0.0001,
             validation_fraction=0.1, verbose=0, warm_start=False)

plt.figure(figsize=(11,4))


plt.subplot(121)

plot_predictions([gbrt], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="앙상블의 예측")

plt.title("learning_rate={}, n_estimators={}".format(gbrt.learning_rate, gbrt.n_estimators), fontsize=14)


plt.subplot(122)

plot_predictions([gbrt_slow], X, y, axes=[-0.5, 0.5, -0.1, 0.8])

plt.title("learning_rate={}, n_estimators={}".format(gbrt_slow.learning_rate, gbrt_slow.n_estimators), fontsize=14)


plt.show()



learning_rate 매개변수가 각 트리의 기여 정도를 조절합니다. 이를 0.1처럼 낮게 설정하면 앙상블을 훈련 세트에 학습 시키기 위해 많은 트리가 필요하지만, 일반적으로 예측 성능이 좋습니다. 이는 축소(shrinkage) 라고 부르는 규제 방법입니다.

왼쪽은 훈련 세트를 학습하기에 트리가 충분하지 않은 반면, 오른쪽은 트리가 너무 많아 훈련 세트에 과대적합되었습니다.


최적의 트리 수를 찾기 위해서는, 조기 종료 기법을 사용할 수 있습니다.

간단하게 구현하려면 staged_predict() 메서드를 사용합니다. 이 메서드는 훈련의 각 단계(트리 하나, 트리 두개 등)에서 앙상블에 의해 만들어진 예측기를 순회하는 반복자(iterator)를 반환합니다. 


다음 코드는 120개의 트리로 GBRT 앙상블을 훈련시키고 최적의 트리 수를 찾기 위해 각 훈련 단계에서 검증 오차를 측정합니다.



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)


gbrt_best = GradientBoostingRegressor(max_depth=2,n_estimators=bst_n_estimators, random_state=42)

gbrt_best.fit(X_train, y_train)

GradientBoostingRegressor(alpha=0.9, criterion='friedman_mse', init=None,
             learning_rate=0.1, loss='ls', max_depth=2, max_features=None,
             max_leaf_nodes=None, min_impurity_decrease=0.0,
             min_impurity_split=None, min_samples_leaf=1,
             min_samples_split=2, min_weight_fraction_leaf=0.0,
             n_estimators=55, n_iter_no_change=None, presort='auto',
             random_state=42, subsample=1.0, tol=0.0001,
             validation_fraction=0.1, verbose=0, warm_start=False)

min_error = np.min(errors)


plt.figure(figsize=(11, 4))


plt.subplot(121)

plt.plot(errors, "b.-")

plt.plot([bst_n_estimators, bst_n_estimators], [0, min_error], "k--")

plt.plot([0, 120], [min_error, min_error], "k--")

plt.plot(bst_n_estimators, min_error, "ko")

plt.text(bst_n_estimators, min_error*1.2, "최소", ha="center", fontsize=14)

plt.axis([0, 120, 0, 0.01])

plt.xlabel("트리 개수")

plt.title("검증 오차", fontsize=14)


plt.subplot(122)

plot_predictions([gbrt_best], X, y, axes=[-0.5, 0.5, -0.1, 0.8])

plt.title("최적 모델 (트리 %d개)" % bst_n_estimators, fontsize=14)


plt.show()


많은 수의 트리를 먼저 훈련시키고 최적의 수를 찾는 것 대신에, 실제로 훈련을 중지하는 방법으로 조기 종료를 구현할 수도 있습니다. 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 #조기 종료


print(gbrt.n_estimators)

61

print('최소 검증 MSE : ', min_val_error)

최소 검증 MSE :  0.002712853325235463


GradientBoostingRegressor는 각 트리가 훈련할 때 사용할 훈련 샘플의 비율을 지정할 수 있는 subsample 매개변수도 지원합니다. 예를 들어 subsample = 0.25라고 하면, 각 트리는 무작위로 선택된 25%의 훈련 샘플로 학습됩니다.

아마 추측할 수 있겠지만, 편향이 높아지는 대신 분산이 낮아지게 됩니다. 또한 훈련 속도를 상당히 높입니다.

이런 기법을 확률적 그래디언트 부스팅 (Stochastic Gradient Boosting) 이라고 합니다.


9. 스태킹


이 장의 마지막 앙상블 모델은 스태킹(Stacking, stacked generalization 줄임말) 입니다.

이는 앙상블에 속한 모든 예측기의 예측을 취합하는 간단한 함수 (직접 투표 같은) 를 사용하는 대신, 취합하는 모델을 훈련시킨다는 아이디어로 출발합니다.


여러 예측기에서 각각 다른 값을 예측하면, 마지막 예측기(블렌더, blender 또는 메타 학습기, meta learner)가 이 예측을 입력으로 받아 최종 예측을 만듭니다.


블렌더를 학습시키는 일반적인 방법은 홀드 아웃(hold-out) 세트를 사용하는 것입니다.

훈련 세트를 두 개의 서브셋으로 나누고, 첫 번째 서브셋은 첫번째 레이어의 예측을 훈련 시키기 위해 사용됩니다. 

그 다음, 첫번째 예측기를 사용해 두 번째 세트(홀드 아웃)에 대한 예측을 만듭니다.

예측기들은 훈련하는 동안 이 샘플들을 전혀 보지 못했기 때문에 만들어진 예측은 완전히 새롭게 됩니다.

즉, 첫 번째 레이어의 예측을 가지고 타깃값을 예측하도록 학습됩니다.


안타깝게도 사이킷런은 스태킹을 직접 지원하지 않습니다. 하지만 구현하는 방법은 연습 문제에서 다룹니다.

아니면, brew와 같은 오픈소스 구현을 사용할 수 있습니다. (http://github.com/viisar/brew)


10. XG Boost


책에는 설명되어있지 않은 내용인데요. XG Boost란, 유연하며 병렬 처리가 가능한 그래디언트 부스팅 라이브러리입니다.

즉, 그래디언트 부스팅에 분산/병렬처리가 가능한 것이라 빠른 속도로 처리가 가능합니다.

XG Boost가 지원하는 모델은 다음과 같습니다.


1) Binary Classification

2) Multiclass Classification

3) Regression

4) Learning to Rank


xgboost에서는 tree ensemble을 사용합니다. Tree ensemble은 여러 개의 CART model로 구성되어 있습니다.

선형 모델에서 목적함수는 Training Loss를 최적화하여 학습 데이터에 모델이 최적화되었는지 측정하고,

Regularization을 최적화하여 모델을 단순화하여 복잡도를 감소시키는 것입니다.


저자의 깃허브에 간단한 예제가 있어 참고합니다.

try:
    import xgboost
except ImportError as ex:
    print("에러: xgboost 라이브러리가 설치되지 않았습니다.")
    xgboost = None



if
xgboost is not None:  # 책에는 없음
   
xgb_reg = xgboost.XGBRegressor(random_state=42)
   
xgb_reg.fit(X_train, y_train)
   
y_pred = xgb_reg.predict(X_val)
   
val_error = mean_squared_error(y_val, y_pred)
   
print("검증 MSE:", val_error)

검증 MSE: 0.0028512559726563943

if xgboost is not None:  # 책에는 없음
    xgb_reg.fit(X_train, y_train,
                eval_set=[(X_val, y_val)], early_stopping_rounds=2)
    y_pred = xgb_reg.predict(X_val)
    val_error = mean_squared_error(y_val, y_pred)
    print("검증 MSE:", val_error)


[0]	validation_0-rmse:0.286719
Will train until validation_0-rmse hasn't improved in 2 rounds.
[1]	validation_0-rmse:0.258221
[2]	validation_0-rmse:0.232634
[3]	validation_0-rmse:0.210526
[4]	validation_0-rmse:0.190232
[5]	validation_0-rmse:0.172196
[6]	validation_0-rmse:0.156394
[7]	validation_0-rmse:0.142241
[8]	validation_0-rmse:0.129789
[9]	validation_0-rmse:0.118752
[10]	validation_0-rmse:0.108388
[11]	validation_0-rmse:0.100155
[12]	validation_0-rmse:0.09208
[13]	validation_0-rmse:0.084791
[14]	validation_0-rmse:0.078699
[15]	validation_0-rmse:0.073248
[16]	validation_0-rmse:0.069391
[17]	validation_0-rmse:0.066277
[18]	validation_0-rmse:0.063458
[19]	validation_0-rmse:0.060326
[20]	validation_0-rmse:0.0578
[21]	validation_0-rmse:0.055643
[22]	validation_0-rmse:0.053943
[23]	validation_0-rmse:0.053138
[24]	validation_0-rmse:0.052415
[25]	validation_0-rmse:0.051821
[26]	validation_0-rmse:0.051226
[27]	validation_0-rmse:0.051135
[28]	validation_0-rmse:0.05091
[29]	validation_0-rmse:0.050893
[30]	validation_0-rmse:0.050725
[31]	validation_0-rmse:0.050471
[32]	validation_0-rmse:0.050285
[33]	validation_0-rmse:0.050492
[34]	validation_0-rmse:0.050348
Stopping. Best iteration:
[32]	validation_0-rmse:0.050285

검증 MSE: 0.0025349167568108864


%timeit xgboost.XGBRegressor().fit(X_train, y_train) if xgboost is not None else None

3.47 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)


%timeit GradientBoostingRegressor().fit(X_train, y_train)
9.7 ms ± 129 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)



 블로그 

출처


이 글의 상당 부분은  [핸즈온 머신러닝, 한빛미디어/오렐리앙 제롱/박해선] 서적을 참고하였습니다.


나머지는 부수적인 함수나 메서드에 대해 부족한 설명을 적어두었습니다.

학습용으로 포스팅 하는 것이기 때문에 복제보다는 머신러닝에 관심이 있다면 구매해보시길 추천합니다.


도움이 되셨다면 로그인 없이 가능한

아래 하트♥공감 버튼을 꾹 눌러주세요! 



반응형

'Python (Linux)' 카테고리의 다른 글

46. Python - 7장 연습문제  (0) 2019.02.21
44. Python - 6장 연습문제  (0) 2019.02.12
43. Python - 결정 트리(Decision Tree)  (0) 2019.02.12
42. Python - 5장 연습문제  (0) 2019.02.10
41. Python - SVM 이론 2  (0) 2019.02.10