Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

StudyStudyStudyEveryday

[파이썬머신러닝] 앙상블 학습과 랜덤 포레스트 본문

DataScience/핸즈온 머신러닝 Hands-on ML

[파이썬머신러닝] 앙상블 학습과 랜덤 포레스트

따듯한붕어빵 2022. 5. 7. 00:36

 

 

7. 앙상블 학습과 랜덤 포레스트

7-1. 투표 기반 분류기

7-2. 배깅과 페이스팅

7-3. 랜덤 패치와 랜덤 서브스페이스

7-4. 랜덤 포레스트

7-5. 부스팅

7-6. 스태킹

 

 

 

 

 


여러 예측기(분류나 회귀 모델)로 부터 예측을 수집하면 가장 좋은 모델 하나보다 더 좋은 예측을 얻을 수 있을 것이다.

이때 일련의 예측기를 앙상블이라고 부르기 때문에, 이러한 학습을 앙상블 학습 (ensemble learning)이라고 하며 앙상블 학습 알고리즘을 앙상블 방법(ensemble method)이라고 한다.

 

 

예를 들어, 결정 트리 앙상블인 랜덤 포레스트가 있다. 훈련 세트에서 무작위로 서로 다른 서브셋을 만들어 결정트리 분류기를 훈련시킬 수 있다. 모든 개별 트리의 예측을 구해 가장 많은 선택을 받은 class를 예측으로 삼는다. 간단한 방법임에도 성능이 좋은 머신러닝 알고리즘이다.

 

 

 

1. 투표기반 분류기

정확도가 80%인 분류기 여러 개를 훈련시켰다고 가정하자.

(로지스틱 회귀 분류기, SVM 분류기, 랜덤 포레스트 분류기, K-최근접 이웃 분류기 등)

위에서 훈련시킨 각 분류기의 예측을 모아 가장 많이 선택된 class를 예측하면 더 좋은 분류기를 만들 수 있다.

(직접 투표 분류기 hard voting)

 

=> 이렇게 다수결 투표 분류기가 앙상블에 포함된 개별 분류기 중 가장 뛰어난 것보다 더 정확도가 높은 경우가 많다.

각 분류기가 약한 학습기(weak learner)일지라도 충분히 많고 다양하다면 앙상블은 높은 정확도를 내는 강한 학습기(strong learner)가 될 수 있다.

 

 

Why? 이유는 큰 수의 법칙 (law of large numbers) 때문이다.

동전을 던지는 게임을 예로 들었을 때 동전을 많이 던질 수록 앞면이 나올 확률이 50%에 가까워진다.

 

 

하지만 많은 모델을 사용할수록 정확도가 높아진다는 가정은 모든 분류기가 완벽하게 독립적이고 오차에 상관관계가 없어야 가능하다. 특히 같은 데이터로 모델을 훈련시키는 경우 분류기들이 같은 종류의 오차를 만들기 쉽기 때문에 잘못된 class가 다수인 경우가 많고 앙상블 정확도는 낮아진다.

 

 

다음은 여러 분류기를 조합해 sklearn의 투표 기반 분류기 (VotingClassifier)을 만들고 훈련시키는 코드이다.

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))
>>>
LogisticRegression 0.864
RandomForestClassifier 0.896
SVC 0.896
VotingClassifier 0.912

예상대로 투표 기반 분류기가 다른 개별 분류기보다 성능이 조금 더 높다.

 

모든 분류기가 class의 확률을 예측할 수 있으면 (predict_proba() 메서드가 있으면), 개별 분류기의 예측을 평균 내어 확률이 가장 높은 class를 예측할 수 있다. (간접 투표 soft voting)

 

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

  • voting='hard' 를 voting='soft'로 두고 모든 분류기가 클래스의 확률을 추정할 수 있으면 된다.
  • SVC는 기본값에서는 class 확률을 제공하지 않으므로 probability = True로 지정해야한다. (단, class 확률 추정을 위해 교차 검증을 사용하므로 훈련 속도가 느려지지만 SVC에서 predict_proba() 메서드를 사용할 수 있다.)
log_clf = LogisticRegression(solver="lbfgs", random_state=42)
rnd_clf = RandomForestClassifier(n_estimators=100, random_state=42)
svm_clf = SVC(gamma="scale", 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)


# 정확도 측정
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.896
SVC 0.896
VotingClassifier 0.92

 

 

 

2. 배깅과 페이스팅

다양한 분류기를 만드는 방법

  • 각기 다른 훈련 알고리즘을 사용
  • 같은 알고리즘을 사용하고 훈련 세트의 서브셋을 무작위로 구성해 분류기를 각기 다르게 학습

 

  • 배깅 (bagging) : 훈련 세트에서 중복을 허용하여 샘플링하는 방식 (bootstrap aggregating의 줄임말)
    • 한 예측기를 위해 같은 훈련 샘플을 여러 번 샘플링할 수 있다.
  • 페이스팅 (pasting) : 훈련 세트에서 중복을 허용하지 않고 샘플링하는 방식

=> 배깅과 페이스팅 모두 같은 훈련 샘플을 여러 개의 예측기에 걸쳐 사용할 수 있다.

=> 모든 예측기가 훈련을 마치면 앙상블은 모든 예측기의 예측을 모아 샘플에 대한 예측을 만든다.

(수집 함수는 전형적으로 분류일 때 통계적 최빈값 (statistical mode)이고 회귀에서는 평균을 계산한다.)

 

=> 일반적으로 앙상블의 결과는 원본 데이터셋으로 하나의 예측기를 훈련시킬 때와 비교해 편향은 비슷하지만 분산은 줄어든다.

 

 

* 예측기는 모두 동시에 다른 CPU 코어나 서버에서 병렬로 학습시킬 수 있다. (예측도 병렬로 수행할 수 있다.) 이런 확장성 덕분에 배깅과 페이스팅의 인기가 높다.

 

 

 

2-1. 사이킷런의 배깅과 페이스팅

사이킷런은 BaggingClassifier (BaggingRegressor)을 제공한다.

(페이스팅을 쓰려면 bootstrap=False 로 지정)

 

다음은 결정 트리 분류기 500개의 앙상블을 훈련시키는 코드이다.

각 분류기는 훈련 세트에서 중복을 허용하여 무작위로 선택된 100개의 샘플로 훈련된다.

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)
  • n_jobs : 훈련과 예측에 사용할 CPU 코어 수 지정 (-1로 지정하면 가용한 모든 코어 사용)

 

 

다음은 단일 결정 트리의 결정 경계와 500개의 트리를 사용한 배깅 앙상블의 결정 경계를 비교한 것이다.

(왼쪽)단일 결정 트리, (오른쪽)500개의 트리로 만든 배깅 앙상블

위에서 볼 수 있듯이 앙상블의 예측이 결정 트리 하나의 예측보다 일반화가 훨씬 잘 된다.

앙상블은 비슷한 편향에서 더 작은 분산을 만든다. (훈련 세트의 오차 수가 거의 비슷하지만 결정 경계는 덜 불규칙)

 

bootstrapping (중복을 허용한 resampling)은 각 예측기가 학습하는 서브셋에 다양성을 증가시키므로 배깅이 페이스팅보다 편향이 조금 더 높다. 하지만 예측기들의 상관관계가 줄어들어 앙상블의 분산을 감소시킨다.

 

전반적으로 배깅이 더 나은 모델을 만들지만, 시간과 CPU에 여유가 있다면 교차 검증으로 배깅과 페이스팅 모두 평가해 더 나은 쪽을 선택하는 것이 좋다.

 

 

2-2. oob 평가

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

BaggingClassifier은 기본값으로 중복을 허용하여 훈련 세트의 크기 m 만큼 샘플을 선택한다.

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

선택되지 않은 훈련 샘플의 나머지 37%oob(out-of-bag) 샘플이라고 부른다. 예측기마다 남겨진 37%는 모두 다르다.

 

예측기가 훈련되는 동안 oob샘플을 사용하지 않으므로 별도의 검증 세트를 쓸 필요 없이 oob 샘플을 이용해 평가할 수 있다. 앙상블의 평가는 각 예측기의 oob평가를 평균하여 얻는다.

 

사이킷런의 BaggingClassifier에서 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_
>>> 0.8986666666666666

 

oob 평가 결과는 다음과 같다.

from sklearn.metrics import accuracy_score
y_pred = bag_clf.predict(X_test)
accuracy_score(y_test, y_pred)
>>> 0.912

테스트 세트에서 91.2%의 정확도를 얻었다.

 

 

  • oob_decision_function_ : oob 샘플에 대한 결정 함수 값 확인가능
    • 결정 함수는 각 훈련 샘플의 클래스 확률을 반환한다.
bag_clf.oob_decision_function_
>>>
array([[0.32275132, 0.67724868],
       [0.34117647, 0.65882353],
       [1.        , 0.        ],
       ...

여기서 첫 번째 훈련 샘플이 양성 클래스일 확률 68.25%, 음성 클래스일 확률 32.27% 이다.

 

 

 

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

BaggingClassifier은 특성 샘플링도 지원한다.

  • max_features, bootstrap_features 로 샘플링 조절
  • max_samples, bootstrap과 작동 방식은 동일하지만 샘플이 아니고 특성에 대한 샘플링이다.
  • 따라서 각 예측기는 무작위로 선택한 입력 특성의 일부분으로 훈련된다.

=> 이미지와 같은 고차원 데이터셋을 다룰 때 유용하다.

 

  • 랜덤 패치 방식 (random patches method) : 훈련 특성과 샘플을 모두 샘플링
  • 랜덤 서브스페이스 방식 (random subspaces method) : 훈련 샘플을 모두 사용 (bootstrap=False, max_samples=1.0), 특성은 샘플링하는 것 (bootstrap_features=True, max_features <= 1.0으로 설정)

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

 

 

4. 랜덤 포레스트

  • 랜덤 포레스트 : 배깅 (or 페이스팅)을 적용한 결정 트리의 앙상블
    • 전형적으로 max_samples를 훈련 세트의 크기로 지정
    • RandomForestClassifier 사용 (or RandomForestRegressor)

 

다음은 최대 16개의 리프 노드를 갖는 500개 트리로 이루어진 랜덤 포레스트 분류기를 여러 CPU 코어에서 훈련시키는 코드이다.

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)

RandomForestClassifier는 몇 가지 예외를 제외하고는 DecisionTreeClassifier와 BaggingClassifier의 매개변수를 모두 가지고 있다.

 

랜덤 포레스트 알고리즘은 트리의 노드를 분할할 때 전체 특성 중 최선의 특성을 찾는 대신 무작위로 선택한 특성 후보 중 최적의 특성을 찾는 식으로 무작위성을 더 주입한다.

=> 트리가 더 다양해지고, 편향을 손해보는 대신 분산을 더 낮춰 전체적으로 더 훌륭한 모델을 만든다.

 

다음은 BaggingClassifier을 사용해 앞의 RandomForestClassifier와 거의 유사하게 만든 것이다.

bag_clf = BaggingClassifier(
    DecisionTreeClassifier(max_features="sqrt", max_leaf_nodes=16),
    n_estimators=500, random_state=42)
    
bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)

 

 

4-1. 엑스트라 트리

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

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

 

  • 익스트림 랜덤 트리 (extremely randomized trees) 앙상블 = 엑스트라 트리 (extra-trees) : 극단적으로 무작위한 트리의 랜덤 포레스트
    • 편향이 늘어나지만 대신 분산은 낮아진다.
    • 최적의 임곗값을 찾는 것이 시간이 많이 걸리는 것이기 때문에, 엑스트라 트리는 속도가 다른 랜덤 포레스트보다 훨씬 빠르다.
    • ExtraTreesClassifier을 사용한다.

 

 

4-2. 특성 중요도

랜덤 포레스트는 특성의 상대적 중요도를 측정하기 쉽다. 사이킷런은 어떤 특성을 사용한 노드가 모든 트리에 걸쳐 평균적으로 불순도를 얼마나 감소시키는지 확인하여 특성의 중요도를 측정한다.

즉, 특성의 상대적 중요도 = 가중치 평균이며 각 노드의 가중치 = 연관된 훈련 샘플 수

 

 

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

다음은 iris데이터셋에 RandomForestClassifier을 훈련시키고 각 특성의 중요도를 출력한 결과이다.

from sklearn.datasets import load_iris
iris = load_iris()
rnd_clf = RandomForestClassifier(n_estimators=500, 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.11249225099876375
sepal width (cm) 0.02311928828251033
petal length (cm) 0.4410304643639577
petal width (cm) 0.4233579963547682

가장 중요한 특성은 꽃잎의 길이(44%)와 너비(42%)이다.

 

다음은 MNIST 데이터셋에 랜덤 포레스트 분류기를 훈련시키고 각 픽셀의 중요도를 그래프로 나타낸 결과이다.

랜덤포레스트는 특히 특성을 선택할 때 어떤 특성이 중요한지 빠르게 확인할 수 있어 편리하다.

 

 

 

5. 부스팅

  • 부스팅 (boosting) : 약한 학습기를 여러 개 연결하여 강한 학습기를 만드는 앙상블 방법
    • 앞으로 모델을 보완해나가면서 일련의 예측기를 학습시키는 것
    • AdaBoost (adaptive boosting), Gradient Boosting 등이 있다.

 

5-1. 에이다부스트 (AdaBoost)

이전의 예측기를 보완하는 새로운 예측기를 만드는 방법

=> 이전 모델이 과소적합했던 훈련 샘플의 가중치를 높임

=> 새로운 예측기는 학습하기 어려운 샘플에 점점 더 맞춰짐

 

 

예를 들어 AdaBoost 분류기를 만들 때

  1. 먼저 알고리즘이 기반이 되는 첫 번째 분류기를 훈련 세트에 훈련시키고 예측을 만든다.
  2. 알고리즘이 잘못 분류된 훈련 샘플의 가중치를 상대적으로 높인다.
  3. 두 번째 분류기는 업데이트된 가중치를 사용해 훈련 세트에서 훈련하고 다시 예측을 만든다.
  4. 그 다음 다시 가중치를 업데이트하는 방식으로 반복한다.

즉, 샘플의 가중치를 업데이트하면서 순차적으로 학습

 

 

이런 연속된 학습 기법은 경사하강법과 비슷한 면이 있다.

경사하강법은 비용 함수를 최소화하기 위해 한 예측기의 모델 파라미터를 조정해가는 반면, AdaBoost는 점차 더 좋아지도록 앙상블에 예측기를 추가한다.

 

연속된 예측기의 결정 경계

 

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

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

 

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

 

 

AdaBoost 알고리즘을 더 자세히 알아보자.

  1. AdaBoost의 각 샘플 가중치 \(w^{(i)}\)는 초기에 \(1/m\)로 초기화된다.
  2. 첫 번째 예측기가 학습되고, 가중치가 적용된 에러율 \(r_1\)이 훈련 세트에 대해 계산된다. ( (1) 이용 )
  3. 예측기의 가중치 \(\alpha_j\)가 계산된다. ( (2) 이용 )
  4. AdaBoost 알고리즘이 가중치 업데이트 규칙을 이용해 샘플의 가중치를 업데이트한다. (잘못 분류된 샘플의 가중치 증가)
  5. 모든 샘플의 가중치 정규화 (\(\sum_{i=1}^{m}w^{(i)}\)로 나눈다.)
  6. 새 예측기가 업데이트된 가중치를 사용해 훈련되고 전체 과정 반복
  7. 지정된 예측기의 수에 도달하거나 완벽한 예측기가 만들어지만 중지

 

(1)  j 번째 예측기의 가중치가 적용된 에러율

$$ r_j = \frac{\underset{\hat{y}_j^{(i)} \neq y^{(i)}}{\sum_{i=1}^{m}w^{(i)}}}{\sum_{i=1}^{m}w^{(i)}} $$

여기서 \(\hat{y}_j^{(i)}\)는 i번째 샘플에 대한 j번째 예측기의 예측

 

=> 즉, j번째 예측기의 예측이 실제값과 비교해서 얼마나 잘못되었는지에 대한 에러율

 

(2)  예측기 가중치

$$ \alpha_j = \eta \ log \frac{1-r_j}{r_j} $$

\(\eta\)의 기본값 = 1

=> 에러율에 비해 잘 예측된 상대 정도를 가중치에 반영 (분자가 잘 예측된 정도, 분모가 잘 예측되지 않은 정도)

=> 예측기가 정확할수록 가중치가 높아지게 된다. (무작위 예측이면 가중치가 0에 가깝고, 무작위보다 더 예측기가 안 좋으면 가중치는 음수가 된다.)

 

(3)  가중치 업데이트 규칙

$$ w^{(i)} \leftarrow \left\{\begin{align}
&w^{(i)} \ \ & \hat{y}_j^{(i)} = y^{(i)}\\
&w^{(i)}exp(\alpha_j) \ \ & \hat{y}_j^{(i)} \neq y^{(i)} \end{align}\right. $$

여기서 \(i = 1, \cdots, m\)

=> 잘못 분류된 샘플의 가중치 증가

 

 

이제 예측기가 학습되었으니 예측을 해보자.

 

예측을 할 때 AdaBoost는 단순히 모든 예측기의 예측을 계산하고 예측기 가중치 \(\alpha_j\)를 더해 예측 결과를 만든다. 그래서 가중치 합이 가장 큰 class가 예측 결과가 된다.

 

[ AdaBoost 예측 ]

$$ \hat{y}(\mathbf{x}) = \underset{k}{argmax}\underset{\hat{y}_j(\mathbf{x}=k)}{\sum_{j=1}^{N}}\alpha_j $$

여기서 N은 예측기 수이다.

 

 

사이킷런은 SAMME라는 AdaBoost의 Multiclass 버전을 사용한다. (class=2인 경우는 SAMME와 AdaBoost가 동일)

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

 

 

다음 코드는 사이킷런의 AdaBoostClassifier (or AdaBoostRegressor)을 사용해 200개의 아주 얕은 결정 트리를 기반으로 하는 AdaBoost 분류기를 훈련 시킨다. (사용하는 결정트리는 max_depth=1)

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)

만약 AdaBoost 앙상블이 훈련 세트에 과대적합되면 추정기 수를 줄이거나 추정기의 규제를 더 강하게 하면 된다.

 

 

5-2. 그레이디언트 부스팅 (Gradient Boosting)

그레이디언트 부스팅 : 앙상블에 이전까지의 오차를 보정하도록 예측기를 순차적으로 추가한다.

단, AdaBoost처럼 반복마다 샘플의 가중치를 수정하는 대신 이전 예측기가 만든 '잔여 오차 (residual error)'에 새로운 예측기를 학습시킨다.

 

 

결정 트리 기반 예측기를 사용하는 간단한 회귀 문제 (gradient tree boosting or gradient boosted regression tree, GBRT)를 풀어보겠다.

# 잡음이 섞인 2차 곡선 형태의 훈련 세트 생성
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)


# 첫 번째 예측기에서 생긴 잔여 오차에 두 번째 DeicisionTreeRegressor 훈련
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)

# 세 개의 트리를 포함하는 앙상블 모델을 이용해 새로운 샘플에 대한 예측
# 모든 트리의 예측을 더해준다.
X_new = np.array([[0.8]])
y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))
y_pred
>>> array([0.75026781])

 

(왼쪽) 세 트리의 예측, (오른쪽) 앙상블의 예측

  • 첫 번째 행 : 앙상블에 트리가 하나만 있어 첫 번째 트리와 예측이 같다.
  • 두 번째 행 : 새로운 트리가 첫 번째 트리의 잔여 오차에 대해 학습되었다. 오른쪽 앙상블 예측이 두 개의 트리 예측의 합과 같은 것을 알 수 있다.
  • 세 번째 행 : 또 다른 트리가 두 번째 트리의 잔여 오차에 훈련 되었다. 트리가 앙상블에 추가될수록 앙상블의 예측이 점차 좋아지는 것을 알 수 있다.

 

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

from sklearn.ensemble import GradientBoostingRegressor

gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0, random_state=42)
gbrt.fit(X, y)
  • n_estimaoter : 앙상블의 훈련 제어 매개변수 (트리 수)
  • max_depth, min_samples_leaf : 결정 트리의 성장을 제어하는 매개변수
  • learning_rate : 각 트리의 기여 정도 조절
    • 낮게 설정하면 앙상블을 훈련 세트에 학습시키기 위해 많은 트리가 필요하지만 일반적으로 예측의 성능은 좋아진다. (축소 shrinkage 라는 규제 방법이다.)

 

(왼쪽) 예측기가 부족한 경우, (오른쪽) 예측기가 너무 많은 경우의 GBRT 앙상블

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

 

그렇다면 최적의 트리 수는 어떻게 찾을까?

=> 조기종료 기법을 사용할 수 있다.

=> staged_predict() : 훈련의 각 단계에서 앙상블에 의해 만들어진 예측기를 순회하는 반복자 (iterator)을 반환한다.

 

 

1) 많은 수의 트리를 훈련 시켜 최적의 수를 찾는다.

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

마지막에 최적의 트리 수를 사용해 새로운 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) + 1

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

 

검증오차와 최적 모델의 예측을 시각화하는 코드는 다음과 같다.

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

plt.subplot(121)
plt.plot(np.arange(1, len(errors) + 1), 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, "Minimum", ha="center", fontsize=14)
plt.axis([0, 120, 0, 0.01])
plt.xlabel("Number of trees")
plt.ylabel("Error", fontsize=16)
plt.title("Validation error", fontsize=14)

plt.subplot(122)
plot_predictions([gbrt_best], X, y, axes=[-0.5, 0.5, -0.1, 0.8])
plt.title("Best model (%d trees)" % bst_n_estimators, fontsize=14)
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.xlabel("$x_1$", fontsize=16)

save_fig("early_stopping_gbrt_plot")
plt.show()

조기 종료를 사용하여 트리 수 튜닝

 

 

2) 실제로 훈련을 중지하는 방법으로 조기 종료 구현

warm_start=True : 사이킷런이 fit()메서드가 호출될 때 기존 트리를 유지하고 훈련을 추가할 수 있도록 해준다.

 

다음 코드는 5번의 반복동안 검증오차가 향상되지 않으면 훈련을 멈춘다.

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
  • subsample : 각 트리가 훈련 시 사용할 훈련 샘플의 비율 지정
    • 편향이 높아지는 대신 분산이 낮아진다.
    • 훈련 속도를 상당히 높여준다.

=> 이러한 기법을 확률적 그레이디언트 부스팅 (stochastic gradient boosting) 이라고 한다.

 

최적화된 Gradient boosting 구현으로 XGBoost가 유명하다. (매우 빠른속도, 확장성, 이식성)

try:
    import xgboost
except ImportError as ex:
    print("에러: xgboost 라이브러리 설치되지 않았습니다.")
    xgboost = None
    
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("Validation MSE:", val_error)
>>>
[0]	validation_0-rmse:0.22834
[1]	validation_0-rmse:0.16224
[2]	validation_0-rmse:0.11843
[3]	validation_0-rmse:0.08760
[4]	validation_0-rmse:0.06848
[5]	validation_0-rmse:0.05709
[6]	validation_0-rmse:0.05297
[7]	validation_0-rmse:0.05129
[8]	validation_0-rmse:0.05155
[9]	validation_0-rmse:0.05211
Validation MSE: 0.002630868681577655

XGBoost는 자동 조기 종료와 같은 기능도 제공한다.

 

 

 

6. 스태킹 (Stacking)

스태킹은 stacked generalization의 줄임말로, '앙상블에 속한 모든 예측기의 예측을 취합하는 간단한 함수를 사용하는 대신 취합하는 모델을 훈련시킬 수는 없을까?' 라는 아이디어로 출발한다.

n개의 예측기가 서로 다른 값을 예측하면 마지막 예측기 (블렌더 blender or 메타 학습기 meta learner)가 이 예측을 입력으로 받아 최종 예측을 만든다.

 

 

블렌더 훈련

블렌더를 학습 시키는 방법

=> 홀드 아웃 hold-out 세트 사용

  1. 훈련 세트를 두 개의 서브셋(subset1, hold-out set)으로 나눈다.
  2. 첫 번째 서브셋(subset1)은 첫 번째 레이어의 예측을 훈련시키기 위해 사용된다.
  3. 첫 번째 레이어의 예측기를 사용해 두 번째 세트(hold-out set)에 대한 예측을 만든다.
  4. hold-out set의 각 샘플에 대한 여러 예측값 중 target값은 그대로 쓰고 앞에서 예측한 값을 입력 특성을 사용하는 새로운 훈련 세트를 만들 수 있다. (새로운 훈련 세트는 3차원)
  5. 블랜더가 새 훈련 세트로 훈련된다. 즉, 첫 번째 레이어의 예측을 가지고 target 값을 예측하도록 학습된다.

 

이러한 블렌더를 여러 개 훈련시키는 것도 가능하다. (예를 들어 하나는 선형 회귀 하나는 랜덤 포레스트 회귀)

그러면 블렌더만의 레이어가 만들어진다.

  1. 훈련 세트를 세 개의 서브셋으로 나눈다.
  2. 첫 번째 세트는 레이어를 훈련시킨다.
  3. 두 번째 세트는 첫 번째 레이어의 예측기로 두 번째 레이어를 훈련시키기 위한 훈련 세트를 만드는 데 사용한다.
  4. 세 번째 세트는 두 번째 레이어의 예측기로 세 번째 레이어를 훈련시키기 위한 훈련 세트를 만드는 데 사용한다.
  5. 작업이 끝나면 각 레이어를 차례대로 실행해 새로운 샘플에 대한 예측을 만들 수 있다.

 

하지만 사이킷런은 스태킹을 직접 지원하지 않아 직접 구현해야 한다.

다음은 블렌더를 훈련시킨 후 분류기를 모아 스태킹 앙상블을 만드는 코드이다.

X_val_predictions = np.empty((len(X_val), len(estimators)), dtype=np.float32)

for index, estimator in enumerate(estimators):
    X_val_predictions[:, index] = estimator.predict(X_val)
    
X_val_predictions
>>>
array([[5., 5., 5., 5.],
       [8., 8., 8., 8.],
       [2., 2., 3., 2.],
       ...,
       [7., 7., 7., 7.],
       [6., 6., 6., 6.],
       [7., 7., 7., 7.]], dtype=float32)
rnd_forest_blender = RandomForestClassifier(n_estimators=200, oob_score=True, random_state=42)
rnd_forest_blender.fit(X_val_predictions, y_val)

rnd_forest_blender.oob_score_
>>> 0.9707

 

이 블렌더를 세밀하게 튜닝하거나 다른 종류의 블렌더(예를 들어 MLPClassifier)를 시도해 볼 수 있다.

그런 다음 교차 검증을 사용해 가장 좋은 것을 선택한다.

 

다음으로 테스트세트에서 앙상블을 평가해보겠다.

X_test_predictions = np.empty((len(X_test), len(estimators)), dtype=np.float32)

for index, estimator in enumerate(estimators):
    X_test_predictions[:, index] = estimator.predict(X_test)

y_pred = rnd_forest_blender.predict(X_test_predictions)

from sklearn.metrics import accuracy_score
accuracy_score(y_test, y_pred)
>>> 0.9695

 

 

 


* 해당 포스팅은 머신러닝 학습 중 핸즈온 머신러닝 2판을 참고하여 작성하였습니다. *

 

 

 

 

Comments