Networks/Hands-On 머신러닝 정리

Hands-On Machine Learning 정리 - 머신러닝(Chapter 7: 앙상블 학습과 랜덤 포레스트)

코딩하는 Español되기 2024. 10. 21. 18:00

○ 대중의 지혜(Wisdom of the Crowd)

: 무작위로 선택된 수천 명의 사람에게 복잡한 질문을 할 때 모은 답이 전문가의 답이 낫다.

○ 앙상블 학습(Ensemble Learning)

: 일련의 예측기로부터 예측을 수집하면 가장 좋은 모델 하나보다 더 좋은 예측을 얻을 수 있을 것. 이러한 일련의 예측기 

○ 앙상블 방법(Ensemble Method)

: 훈련 세트로부터 무작위로 각기 다른 서브셋을 만들어 일련의 결정 트리를 훈련 가능
  (예측을 하려면 모든 개별 트리의 예측을 구하기) → 가장 많은 선택을 받은 클래스가 예측

○ 랜덤 포레스트(Random Forest): 결정 트리의 앙상블

해당 챕터에서는 랜덤 포레스트, 배깅, 부스팅, 스태킹 등 인기 있는 앙상블 방법을 설명합니다.


Chap7 목차

 

1. 투표 기반 분류기

2. 배깅과 페이스팅

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

    - oob(Out-of-Bag) 평가

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

4. 랜덤 포레스트

    - 엑스트라 트리

    - 특성 중요도

5. 부스팅

    - Ada Boosting

    - Gradient Boosting

6. 스태킹


1. 투표 기반 분류기

○ 정확도가 80%인 분류기를 여러 개 훈련시켰다는 가정(로지스틱 회귀, SVM 분류기, 랜덤 포레스트 분류기 등)

    ● 더 좋은 분류기를 만드는 방법?

    ● 직접 투표(Hard Voting) 분류기 : 각 분류기의 예측을 모아서 가장 많이 선택된 클래스를 예측하기

        - 앙상블에 포함된 개별 분류기 中 가장 뛰어난 것보다 정확도가 높을 경우가 多

        - 각 분류기가 약한 학습기(랜덤 추측보다 조금 더 높은 성능을 내는 분류기)일지라도 충분하게 많고 다양하다면?

        - 강한 학습기가 될 수 있음

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

    ● How? 각기 다른 알고리즘으로 학습시키기(매우 다른 종류의 오차를 만들 가능성 多 ∴ 모델 정확도 상승)

○ 여러 분류기를 조합하여 사이킷런 의 VotingClassifier를 만들고 훈련시키는 코드

    ● 데이터셋: moons 데이터셋(5장)

더보기

- 데이터셋 생성

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)

 

- 모델 훈련

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() 메서드가 있으면)

    ● 개별 분류기의 예측을 평균 내어 확률이 가장 높은 클래스를 예측 가능(간접 투표)

    ● 직접 투표 방식보다 성능이 높음 ∵ 해당 방법의 경우 확률이 높은 투표에 비중을 더 두기 때문

    ● How to use? voting="soft"로 변경 & 모든 분류기가 클래스의 확률을 추정 가능하면 됨

    ※ SVC는 기본값에서는 클래스 확률을 제공하지 않음 So, probability 매개변수를 true로 지정

[간접 투표 코드]

더보기

모델 훈련

    - 로지스틱 회귀, 랜덤 포레스트, SVC

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

○ 예시

    ● 동전을 던졌을 때 앞면 51%, 뒷면 49%가 나오는 동전이 있다는 가정

    ● 1,000번 던진다면 대략 510번은 앞면, 490번은 뒷면으로 다수는 앞면

    ● 1,000번을 던진 후 앞면이 다수가 될 확률은 75%에 가깝다는 계산이 됨(더 많이 던질수록 확률은 증가)

    ● 10,000번을 던진다면 확률이 97%까지 상승함(∵ 큰 수의 법칙) 

[큰 수의 법칙 사진 코드]

더보기
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("Number of coin tosses")
plt.ylabel("Heads ratio")
plt.legend(loc="lower right")
plt.axis([0, 10000, 0.42, 0.58])
save_fig("law_of_large_numbers_plot")
plt.show()

[10,000번 던졌을 때 나오는 확률 계산 공식]

○ 이항 분포(Binomial Distribution)의 확률 질량 함수(Probability Mass Function)로 계산 가능

○ 확률이 p인 이항 분포에서 n번의 시도 中 k번 성공할 확률

$$ \frac{n}{k}p^{k}(1-p)^{n-k} $$

○ 성공 확률이 51%인 동전을 1,000번 던져서 앞면이 한 번만 나올 확률

$$ \frac{1000}{1}0.51^{1}(1-0.51)^{1000-1}=1.6*10^{-307} $$

○ 이런 식으로 1,000의 과반 직전이 499까지의 확률을 더해 전체 확률 1에서 빼면 1,000번 던져 절반 이상 나올 확률

○ 사이파이(이항 분포의 누적 분포 함수 존재)에서 계산

from scipy.stats import binom

1-binom.cdf(499,1000, 0.51) # 0.747

# 10,000번 던져 앞면이 절반 이상 나올 확률
1-binom.cdf(4999,10000, 0.51) # 0.978

 

2. 배깅과 페이스팅

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

    [방법 1] 각기 다른 훈련 알고리즘 사용(투표 기반 분류기)

    [방법 2] 같은 알고리즘을 사용하고 훈련 세트의 서브셋을 무작위로 구성해 분류기를 각기 다르게 학습시키기

        ● 배깅(Bootstrap Aggregating; Bagging): 훈련 세트에서 중복을 허용하여 샘플링하는 방식

        ※ 부트스트래핑(Bootstrapping): 중복을 허용한 Resampling(In 통계학)

        ● 페이스팅(Pasting): 중복을 허용하지 않고 샘플링하는 방식

        ● 즉, 배깅과 페이스팅에서는 같은 훈련 샘플을 여러 개의 예측기에 걸쳐 사용 가능

        ● But, 배깅만이 한 예측기를 위해 같은 훈련 샘플을 여러 번 샘플링 가능

○ 모든 예측기가 훈련을 마치면 앙상블은 모든 예측기의 예측을 모아서 새로운 샘플에 대한 예측 생성

    ● 수집 함수(전형적인 경우)

        - 분류일 경우: 통계적 최빈값(Statistical Mode; 가장 많은 예측 결과 like 직접 투표)

        - 회귀일 경우: 평균 계산

    ● 개별 예측기는 원본 훈련 세트로 훈련시킨 것보다 크게 편향되어 있음

    ● But 수집 함수를 통과하면 편향과 분산이 모두 감소

일반적으로 앙상블의 결과: 원본 데이터셋으로 하나의 예측기를 훈련시킬 때와 비교해 편향은 비슷하지만 분산은 감소

※ 예측기는 모두 동시에 다른 CPU코어나 서버에서 병렬로 학습 가능

 

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

BaggingClassifier(BaggingRegressor): 사이킷런에서 배깅과 페이스팅을 사용하기 위한 API

[결정 트리 분류기 500개의 앙상블을 훈련시키는 코드]

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

    ※ 페이스팅 사용: bootstrap=False로 지정

    ● n_jobs 매개변수: 사이킷런이 훈련과 예측에 사용할 CPU 코어수(-1로 하면 가용한 모든 코어 사용)

    ● BaggingClassifier이 기반이 되는 분류기가 결정 트리 분류기처럼 클래스 확률 추정이 되면

      (= predict_proba() 함수가 있으면) 직접 투표 대신 자동으로 간접 투표 방식을 사용

더보기
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)
from sklearn.metrics import accuracy_score
print(accuracy_score(y_test, y_pred)) # 0.904
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

○ 단일 결정 트리의 결정 경계(이전 코드)와 500개의 트리를 사용한 배깅 앙상블의 결정 경계 비교

    ● 데이터셋: moons 데이터셋

    ● 앙상블 예측이 결정 트리 하나의 예측보다 일반화가 더 잘된 것으로 보임

    ● 앙상블은 비슷한 편향에서 더 작은 분산

    ● 훈련 세트의 오차 수가 거의 비슷 But 결정 경계가 덜 불규칙

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

    ● 다양성을 추가한다는 것 = 예측기들의 상관관계를 줄임 ∴ 앙상블의 분산이 감소

더보기
from matplotlib.colors import ListedColormap

def plot_decision_boundary(clf, X, y, axes=[-1.5, 2.45, -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)
fig, axes = plt.subplots(ncols=2, figsize=(10,4), sharey=True)
plt.sca(axes[0])
plot_decision_boundary(tree_clf, X, y)
plt.title("Decision Tree", fontsize=14)
plt.sca(axes[1])
plot_decision_boundary(bag_clf, X, y)
plt.title("Decision Trees with Bagging", fontsize=14)
plt.ylabel("")
plt.show()

2-2  oob 평가

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

○ BaggingClassifier는 기본값으로 중복을 허용하여 훈련 세트의 크기만큼인 m개 샘플을 선택

   (= 평균적으로 각 예측기에 훈련 샘플의 63% 정도만 샘플링된다는 것을 의미)

oob(out-of-bag): 선택되지 않은 훈련 샘플의 나머지 37%(예측기마다 남겨진 37%는 모두 다름)

예측기가 훈련되는 동안에는 oob 샘플 사용 안 함 So, 별도의 검증 세트를 사용하지 않고 oob 샘플로 평가 가능

○ 앙상블 평가: oob 평가를 평가하여 나온 값

○ 사이킷런에서 BaggingClassifier를 만들 때, oob_score=True로 지정하면 훈련이 끝난 후 자동으로 oob 평가를 진행

    ● oob_score_ : 평가 점수 결과 저장된 변수

    ● oob_decision_function_ : oob 샘플에 대한 결정 함수의 값이 저장된 변수

       ※ 해당 결정 함수는 각 훈련 샘플의 클래스 확률을 반환(∵ predict_proba() 메서드가 존재)

더보기

- 평가 점수 결과

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 평가 결과(테스트 세트에서 정확도)

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

 

- 결정 함숫값 출력(각 훈련 샘플의 클래스 확률 반환)

bag_clf.oob_decision_function_

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

○ BaggingClassifier는 특성 샘플링도 지원

    ● max_features, bootstrap_features 두 매개변수로 샘플링 조절

    ● 작동 방식은 max_samples, bootstrap과 동일하지만 특성에 대한 샘플링

    ● So, 각 예측기는 무작위로 선택한 입력 특성의 일부분으로 훈련

    ● 매우 고차원(이미지 등)의 데이터셋을 다룰 때 유용

랜덤 패치 방식(Random Patches Method) : 훈련 특성과 샘플을 모두 샘플링하는 것

    ● 훈련 샘플 모두 선택(bootstrap=False, max_samples=1.0)

    ● 특성은 샘플링(bootstrap_features=True and/or max_features < 1.0)하는 것

    ● 샘플링은 더 다양한 예측기를 만들며 편향을 늘리는 대신 분산을 낮춤(일반적인 앙상블 기법)

 

4. 랜덤 포레스트

○ 랜덤 포레스트는 일반적으로 배깅(| 페이스팅)을 적용한 결정 트리의 앙상블

○ 전형적으로 max_samples를 훈련 세트의 크기로 지정

○ BaggingClassifier에 DecisionTreeClassifier를 넣어 만드는 대신

   결정 트리에 최적화되어 사용하기 편리한 RandomForestClassifier를 사용 가능

랜덤 포레스트 알고리즘은 트리의 노드를 분할할 때

    ● 전체 특성 中 최선의 특성을 찾는 대신

    ● 무작위로 선택한 특성 후보 중에서 최적의 특성을 찾는 식으로 무작위성을 더 주입(6장 참조)

    ● 결국 트리를 더욱 다양하게 만들고 편향을 손해보는 대신 분산을 낮추어 전체적으로 훌륭한 모델을 제작

○ 500개 트리로 이뤄진 랜덤 포레스트 분류기를 여러 CPU 코어에서 훈련

    ● RandomForestClassifier는 몇 가지 예외가 있지만 아래의 매개변수를 모두 포함

        - DecisionTree Classifier의 매개변수

        - BaggingClassifier의 매개변수

 

[코드]

더보기
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)

- BaggingClassifier

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)
np.sum(y_pred == y_pred_rf) / len(y_pred) # 1.0

4-1 엑스트라 트리

○ 익스트림 랜덤 트리(Extremely Randomized Trees; 엑스트라 트리: Extra-Trees)

: 트리를 더욱 무작위 하게 만들기 위해 최적의 임곗값을 찾는 대신

  후보 특성을 사용해 무작위로 분할 후 그중에서 최상의 분할을 선택하는

  극단적으로 무작위한 트리의 랜덤 포레스트

○ ExtraTreesClassifier를 사용(사용법은 RandomForestClassifier와 동일)

※ RandomForestClassifier와 ExtraTreesClassifier 중 어느 것이 더 좋고 나쁠지 예단하기 어렵기에 둘 다 시도해 보고 교차 검증으로 비교해 보는 것이 유일한 방법(그리드 탐색으로 하이퍼파라미터 튜닝)

 

4-2 특성 중요도

○ 랜덤 포레스트의 장점: 특성의 상대적 중요도를 측정하기 쉽다

○ 어떤 특성을 사용한 노드가 평균적으로 불순도를 얼마나 감소시키는지 확인하여 특성의 중요도를 측정

    ● (정확히) 가중치 평균이며 각 노드의 가중치 = 연관된 훈련 샘플 수

[추가]

결정 트리의 특성 중요도
노드에 사용된 특성별로
(현재 노드의 샘플 비율 * 불순도) - (왼쪽 자식 노드의 샘플 비율 * 불순도) - (오른쪽 자식 노드의 샘플 비율 * 불순도)와 같이 계산
→ 특성 중요도 합이 1이 되도록 전체 합으로 나누어 정규화
샘플 비율 = 트리 전체 샘플 수에 대한 비율
랜덤 포레스트의 경우 특성 중요도 = 각 결정 트리의 특성 중요도를 모두 계산하여 더한 후 트리 수로 나눈 것

○ 사이킷런은 훈련이 끝난 뒤 특성마다 자동으로 점수 계산 → 중요도 전체 합 = 1(정규화)

    ● feature_importances_ 변수에 저장

 

○ RandomForestClassifier를 훈련시키고 각 특성의 중요도 출력

    ● 데이터셋: iris 데이터셋

    ● 중요 특성 : 꽃잎의 길이(44%), 너비(42%), 꽃받침의 길이와 너비는 덜 중요

    ● 랜덤 포레스트는 특성을 선택해야 할 때 어떤 특성이 중요한지 빠르게 확인 가능

더보기

 - 특성 중요도

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)
rnd_clf.feature_importances_
# array([0.11249225, 0.02311929, 0.44103046, 0.423358  ])

 

- 15개 결정 트리의 결정 경계를 중첩한 것(앙상블 되면 좋은 결정 경계를 생성)

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_train[indices_with_replacement], y_train[indices_with_replacement])
    plot_decision_boundary(tree_clf, X, y, axes=[-1.5, 2.45, -1, 1.5], alpha=0.02, contour=False)

plt.show()

[랜덤 포레스트 분류기에서 얻은 MNIST 픽셀 중요도]

    ● 데이터셋: MNIST데이터셋

더보기

- 데이터 로드

from sklearn.datasets import fetch_openml

mnist = fetch_openml('mnist_784', version=1)
mnist.target = mnist.target.astype(np.uint8)

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

plt.show()

5. 부스팅(Boosting; Hypothesis Boosting:가설 부스팅)

○ 약한 학습기를 여러 개 연결하여 강한 학습기를 만드는 앙상블 방법

 

5-1 Ada Boosting(Adaptive Boosting)

: 이전 예측기를 보완하는 새로운 예측기를 만드는 방법으로 이전 모델이 과소적합했던 훈련 샘플의 가중치를 높이는 것

e.g. 에이다 부스트 분류기 만들기

    ● 알고리즘이 기반이 되는 첫 번째 분류기를 훈련 세트에서 훈련시키고 예측 생성

    ● 알고리즘이 잘못 분류된 훈련 샘플의 가중치를 상대적으로 높임

    ● 두 번째 분류기: 업데이트된 가중치를 사용해 훈련 세트에서 훈련하고 다시 예측

    ● 다시 가중치 업데이트...

[출처: Gradient boosting Vs AdaBoosting — Simplest explanation of how to do boosting using Visuals and Python Code]

○ 다섯 개의 연속된 예측기의 결정 경계(규제를 강하게 한 RBF 커널 SVM 분류기; 단순 예시용)

    ※ SVM은 속도가 느리고 AdaBoosting과 함께 사용했을 때 불안정한 경향이 있어 AdaBoosting 기반 예측기로는 적합 X

    ● 데이터셋: moons 데이터셋

    ● 첫 번째 분류기: 많은 샘플을 잘못 분류해서 샘플들의 가중치가 높아짐

    ● 두 번째 분류기: 샘플들을 더 정확하게 예측

    ● 오른쪽 그래프: 학습률을 반으로 낮춘 것만 빼고 똑같은 일련의 예측기

        - 즉, 잘못 분류된 샘플의 가중치는 반복마다 절반 정도만 상승

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

        - 경사하강법: 비용 함수를 최소화하기 위해 한 예측기의 모델 파라미터를 조정

        - AdaBoosting: 점차 더 좋아지도록 앙상블에 예측기를 추가

    ● 모든 예측기가 훈련을 마치면 배깅 | 페이스팅과 비슷한 방식으로 예측 생성

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

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

[연속된 예측기의 결정경계 생성 코드]

더보기

- 해당 그래프는 SVC 모델에서 fit 메서드를 호출할 때 sample_weight 매개변수를 사용해 훈련 샘플의 가중치를 부여하여 제작

    ● 왼쪽 그래프는 잘못 분류된 샘플의 가중치를 2배 증가

    ● 오른쪽 그래프는 1.5배 증가

m = len(X_train)

fig, axes = plt.subplots(ncols=2, figsize=(10,4), sharey=True)

for subplot, learning_rate in ((0, 1), (1, 0.5)):
    sample_weights = np.ones(m) / m
    plt.sca(axes[subplot])
    
    for i in range(5):
        svm_clf = SVC(kernel="rbf", C=0.2, gamma=0.6, random_state=42)
        svm_clf.fit(X_train, y_train, sample_weight=sample_weights * m)
        y_pred = svm_clf.predict(X_train)
        r = sample_weights[y_pred != y_train].sum() / sample_weights.sum() # equation 7-1
        alpha = learning_rate * np.log((1 - r) / r) # equation 7-2
        sample_weights[y_pred != y_train] *= np.exp(alpha) # equation 7-3
        sample_weights /= sample_weights.sum() # normalization step
        plot_decision_boundary(svm_clf, X, y, alpha=0.2)
        plt.title("learning_rate = {}".format(learning_rate), fontsize=16)
        
    if subplot == 0:
        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)
    else:
        plt.ylabel("")

plt.show()

5-1-1 AdaBoosting 알고리즘

○ 각 샘플의 가중치 w^{(i)}는 초기에 1/m로 초기화 → 첫 번째 예측기 학습

   → 가중치가 적용된 에러율 r_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)}} $$

 

○ 예측기의 가증치 alpha_j는 아래 식(예측기 가중치)을 사용해 계산

    ● 여기서 \eta : 학습률 하이퍼파라미터(기본값 1)

    ※ 원래 에이다부스트 알고리즘은 학습률 파라미터를 사용 X

    ● 예측기가 정확할수록 가중치가 더 높아짐

        IF 무작위로 예측하는 정도 then 가중치는 0에 가까워질 것

        Elif 그보다 나쁘면(무작위 추측보다 정확도가 낮으면) then 가중치 = 음수

[예측기 가중치]
$$ \alpha_{j}=\eta log\frac{1-r_{j}}{r_{j}} $$

 

○ 그다음 에이다부스트 알고리즘이 아래식(가중치 업데이트 규칙)을 사용해 가중치 업데이트

    ● 즉, 잘못 분류된 샘플의 가중치가 증가

$$ w^{(i)}\leftarrow \begin{cases}w^{(i)}& \hat{y_j}^{(i)}=y^{(i)}일 때\\ w^{(i)}\textup{exp}(a_{j})& \hat{y_j}^{(i)}\neq y^{(i)}일때\end{cases} $$

$$ i = 1,2,..., m $$

 

○ 모든 샘플의 가중치를 정규화(즉, 아래의 식으로 나눔)

$$ \sum_{i=1}^{m}w^{(i)} $$

 

○ 새 예측기가 업데이트된 가중치를 사용해 훈련되고 전체 과정 반복

    ● 지정된 예측기 수에 도달 | 완벽한 예측기 만들어지면 중지

    ● 예측할 때 에이다부스트는 단순히 모든 예측기의 예측 계산 → 예측기 가중치 alpha_j를 더해 예측 결과 생성

    ● 가중치 합이 가장 큰 클래스가 예측 결과

[에이다 부스트 예측]

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

사이킷런은 SAMME라는 에이다부스트의 다중 클래스 버전을 사용
클래스가 두 개뿐일 때는 SAMME가 에이다부스트와 동일
예측기가 클래스의 확률을 추정 가능하다면(predict_proba() 메서드가 있다면)
    ● 사이킷런은 SAMME.R(real)이라는 SAMME의 변종을 사용
○ SAMME.R: 예측값 대신 클래스 확률에 기반하면 일반적으로 성능이 더 좋음

※ 추가

[SAMME 알고리즘에서 예측기 가중치를 구하는 식]

※ 여기서 K=2 라면 에이다 부스트 알고리즘의 예측기 가중치 식과 동일

$$ \alpha_j = \eta(log\frac{1-r_j}{r_j}+ log(K-1)) $$

 

[SAMME.R 알고리즘에서 예측기 가중치를 구하는 식]

$$ \alpha_j = -\eta\frac{K-1}{K}y\textup{log}\hat{y}_j $$

여기서 y가 정답 클래스일 때 1 | -1/(1-K)이고

$$ \hat{y}_j = \textup{j 번째 예측기가 만든 클래스 확률} $$

예측을 할 때는 아래의 공식으로 예측기별 클래스 확률을 계산해서 합한 후 확률이 가장 높은 클래스를 선택

$$ \hat{y}(x)=\underset{k}{\textup{argmax}}\sum_{j=1}^{N}(K-1)(\textup{log}\hat{y}_j-\frac{1}{K}\sum_{k=0}^{k}\hat{y}_j) $$

AdaBoostClassifier의 알고리즘의 매개변수의 기본값 = "SAMME.R"

SAMME 알고리즘을 사용하려면 algorithm="SAMME"로 지정


[AdaBoostClassifier를 사용한 200개의 얇은 결정 트리를 기반하는 에이다부스트 분류기를 훈련]

    ● 결정트리: max_depth=1

    ● 즉, 결정 노드 하나와 리프노드 2개로 이루어진 트리(AdaBoostCassifier의 기본 추정기)

더보기
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)

5-2 Gradient Boosting

○ 앙상블에 이전까지의 오차를 보정하도록 예측기를 순차적으로 추가

○ AdaBoost와 차이점

    ● AdaBoost: 반복마다 샘플의 가중치를 수정

    ● Gradient Boost: 이전 예측기가 만든 잔여 오차(Residual Error)에 새로운 예측기를 학습

○ 결정 트리 기반 예측기로 사용하는 간단한 회귀문제 풀기(아래 두 개는 동일한 말)

    ● Gradient Tree Boosting

    ● Gradient Boosted Regression Tree(GBRT)

○ 그림 해석

    ● 왼쪽 열: 세 개의 트리의 예측

    ● 오른쪽 열: 앙상블의 예측

    ● 첫 번째 행: 앙상블에 트리가 하나만 있어서 첫 번째 트리 예측과 완전히 동일

    ● 두 번째 행: 새로운 트리가 첫 번째 트리의 잔여 오차에 대해 학습됨

    ● 세 번째 행: 또 다른 트리가 두 번째 트리의 잔여 오차에 훈련

    ● 결론: 트리가 앙상블에 추가될수록 앙상블의 예측이 좋아짐

 

○ GBRT앙상블을 간단하게 훈련 (GradientboostingRegressor를 사용)

    ● 트리 수(n_estimators): 앙상블의 훈련을 제어하는 매개변수

    ● max_depth, min_samples_leaf: 결정 트리의 성장을 제어하는 매개변수

    ● learning_rate: 각 트리의 기여 정도 조절

        - IF learning_rate < 0.1: 앙상블을 훈련 세트에 학습시키기 위한 많은 트리가 필요

        - But 예측의 성능은 좋아짐(Shrinkage; 축소 규제기법)

    ● 왼쪽: 훈련 세트를 학습하기에는 트리가 충분하지 않음

    ● 오른쪽: 트리가 너무 많아 훈련 세트에 과적합

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

    ● staged_predict() 메서드 사용 

        - 훈련의 각 단계(트리 하나, 트리 두 개 등)에서 앙상블에 의해 만들어진 예측기를 순회하는 반복자를 반환

    ● 왼쪽: 검증 오차

    ● 오른쪽: 최적 모델의 예측

○ GradientBoosting는 각 트리가 훈련할 때 사용할 훈련 샘플의 비율을 지정 가능(확률적 그레디언트 부스팅)

    ● subsample 매개변수

       IF subsample=0.25 then 각 트리는 무작위로 선택된 25%의 훈련 샘플로 학습됨

    ● 편향이 증가하는 대신 분산이 낮아짐 + 훈련 속도가 빨라짐

○ XGBoost(최적화된 그레디언트 부스팅)

    ● Extreme Gradient Boosting

    ● 빠른 속도, 확장성, 이식성

더보기

- 간단한 2차식 형태의 데이터셋 생성

np.random.seed(42)
X = np.random.rand(100, 1) - 0.5
y = 3*X[:, 0]**2 + 0.05 * np.random.randn(100)

 

○ DecisionTreeRegressor를 훈련 세트(잡음이 섞인 2차 곡선 형태의 훈련 세트)에 학습시키기

from sklearn.tree import DecisionTreeRegressor

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

 

○ 첫 번째 예측기에서 생긴 잔여 오차에 두 번째 DecisionTreeRegressor를 훈련

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

 

○ 두 번째 예측기가 만든 잔여 오차에 세 번째 회귀 모델 훈련

    ● 세 개의 트리를 포함하는 앙상블 모델 생성완료

    ● 새로운 샘플에 대한 예측을 만들려면? 모든 트리의 예측을 더하기

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

 

○ 새로운 샘플에 대한 예측 생성

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

 

- "결정 트리 기반 예측기로 사용하는 간단한 회귀문제 풀기" 시각화 코드

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="Training set")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.title("Residuals and tree predictions", 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="Training set")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.title("Ensemble predictions", 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="Residuals")
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()

 

 - 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)
gbrt_slow = GradientBoostingRegressor(max_depth=2, n_estimators=200, learning_rate=0.1, random_state=42)
gbrt_slow.fit(X, y)
fig, axes = plt.subplots(ncols=2, figsize=(10,4), sharey=True)

plt.sca(axes[0])
plot_predictions([gbrt], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="Ensemble predictions")
plt.title("learning_rate={}, n_estimators={}".format(gbrt.learning_rate, gbrt.n_estimators), fontsize=14)
plt.xlabel("$x_1$", fontsize=16)
plt.ylabel("$y$", fontsize=16, rotation=0)

plt.sca(axes[1])
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.xlabel("$x_1$", fontsize=16)

plt.show()

 

- "조기 종료를 사용하여 트리 수 튜닝" 코드

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

    ● 마지막에 최적의 트리 수를 사용하여 새로운 GBRT 앙상블을 훈련

    ● warm_start=True로 설정하면 fit() 메서드가 호출될 때 기존 트리를 유지하고 훈련을 추가하도록 해줌

    ● 아래 코드는 다섯 번의 반복 동안 검증 오차가 향상되지 않으면 훈련을 멈춤

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

 

 

- "조기 종료를 사용하여 트리 수 튜닝" 시각화 코드

min_error = np.min(errors)

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)

plt.show()

 

- XGBoost 사용

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("Validation MSE:", val_error)
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)
%timeit xgboost.XGBRegressor().fit(X_train, y_train) if xgboost is not None else None
# 1min 12s ± 2.2 s per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit GradientBoostingRegressor().fit(X_train, y_train)
# 44.4 ms ± 8.73 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

 

6. 스태킹(Stacking; Stacked Generalization)

※ 사이킷런은 스태킹을 직접 지원하지는 않음

○ 시작 배경: 앙상블에 속한 모든 예측기의 예측을 취합하는 간단한 함수를 사용하는 대신 취합하는 모델을 훈련시킬 수 없을까?

○ 세 개의 예측기가 각각 다른 값(3.1, 2.7, 2.9)을 예측하고 마지막 예측기(블랜더 | 메타 학습기)가 예측을 입력받아서 최종 예측(3.0)을 예측

○ 블랜더를 학습시키는 방법

    ● 일반적으로 홀드 아웃(hold-out) 세트를 사용

○ 과정

    ● 훈련 세트를 두 개의 서브셋으로 나눔

    ● 첫 번째 서브셋은 첫 번째 레이어의 예측을 훈련시키기 위해 사용

    ● 첫 번째 레이어의 예측기를 사용해 두 번째(홀드 아웃) 세트에 대한 예측 생성

        - 예측기들이 훈련하는 동안 이 샘플들을 전혀 보지 못했기 때문에 이때 만들어진 예측은 완전 새로운 것

        - 홀드 아웃 세트의 각 샘플에 대해 세 개의 예측값이 존재

        - 타깃값: 그대로 사용

        - 앞에서 예측한 값: 입력 특성으로 사용하는 새로운 훈련 세트를 만들 수 있음(새로운 훈련 세트는 3차원)

        - 블렌더는 새 훈련 세트로 훈련됨(즉, 첫 번째 레이어의 예측을 가지고 타깃값을 예측하도록 학습)

    ● 이런 방식의 블렌더를 여러 개 훈련시키는 것도 가능

        e.g. 하나는 선형 회귀, 다른 하나는 랜덤 포레스트 회귀로

                → 블렌더만의 레어이가 생성됨

    ● 어떻게?

        - 훈련 세트를 3 개의 서브셋으로 나눔

        - 첫 번째 세트: 첫 번째 레이어를 훈련시키는 데 사용

        - 두 번째 세트: (첫 번째 레이어의 예측기로) 두 번째 레이어를 훈련시키기 위한 훈련 세트 만드는 데 사용

        - 세 번째 세트: (두 번째 레이어의 예측기로) 세 번째 레이어를 훈련시키기 위한 훈련 세트를 만드는 데 사용

        - 작업이 끝나면 각 레이어를 차례대로 실행해서 새로운 샘플에 대한 예측 생성 가능