앙상블 방법(Ensemble Methods) — 부스팅(Boosting), 배깅(Bagging), 스태킹(Stacking)

약한 학습기(weak learner)를 올바르게 결합하면 강한 학습기(strong learner)가 됩니다. 이것은 비유가 아니라 정리(theorem)입니다.

유형: Build 언어: Python 선수 지식: Phase 2, Lesson 10 (편향-분산 트레이드오프) 소요 시간: 약 120분

학습 목표

  • AdaBoost와 그래디언트 부스팅(Gradient Boosting)을 처음부터 구현하고, 부스팅이 어떻게 편향을 순차적으로 줄이는지 설명합니다.
  • 배깅 앙상블(Bagging Ensemble)을 만들고, 상관이 낮은 모델을 평균내면 편향을 크게 늘리지 않고 분산을 줄일 수 있음을 보입니다.
  • 배깅(Bagging), 부스팅(Boosting), 스태킹(Stacking)이 각각 어떤 오류 성분을 겨냥하는지 비교합니다.
  • 앙상블 다양성(Ensemble Diversity)을 평가하고, 독립적인 약한 학습기가 많아질수록 다수결 정확도가 왜 좋아지는지 설명합니다.

문제

단일 결정 트리(Decision Tree)는 빠르게 학습되고 해석하기 쉽지만 과대적합(overfitting)되기 쉽습니다. 단일 선형 모델은 복잡한 경계에서 과소적합(underfitting)되기 쉽습니다. 완벽한 모델 구조를 만들기 위해 며칠을 쓸 수도 있지만, 불완전한 모델 여러 개를 결합해 각각의 모델보다 더 나은 결과를 얻을 수도 있습니다.

앙상블 방법(Ensemble Methods)은 바로 이 일을 합니다. 표 형식(tabular) 데이터의 Kaggle 대회에서 가장 믿을 만한 기법이고, 많은 운영 ML 시스템에서 쓰이며, 편향-분산 트레이드오프가 실제로 어떻게 작동하는지 보여줍니다. 배깅은 분산을 줄입니다. 부스팅은 편향을 줄입니다. 스태킹은 어떤 입력에서 어떤 모델을 신뢰해야 하는지 학습합니다.

사전 테스트

2문제 · 이 강의를 시작하기 전에 얼마나 알고 있는지 확인해보세요

1.약한 분류기 여러 개를 앙상블로 결합하면 정확도가 좋아지는 이유는 무엇인가요?

2.배깅과 부스팅의 가장 큰 차이는 무엇인가요?

0/2 답변 완료

개념

앙상블이 작동하는 이유

정확도가 p > 0.5인 독립 분류기 N개가 있다고 해봅니다. 다수결의 정확도는 다음과 같습니다.

P(다수결이 맞을 확률) = k > N/2에 대해 C(N,k) * p^k * (1-p)^(N-k)의 합

각각 정확도 60%인 분류기 21개를 다수결로 묶으면 정확도는 약 74%가 됩니다. 101개로 늘리면 약 84%까지 올라갑니다. 모델들이 서로 다른 실수를 만들 때 오류가 서로 상쇄되기 때문입니다.

핵심 요구사항은 다양성(diversity) 입니다. 모든 모델이 같은 실수를 한다면 결합해도 도움이 되지 않습니다. 앙상블은 다음 방식으로 다양한 모델을 만듭니다.

  • 서로 다른 학습 부분집합: 배깅(Bagging)
  • 서로 다른 특성 부분집합: 랜덤 포레스트(Random Forest)
  • 순차적 오류 수정: 부스팅(Boosting)
  • 서로 다른 모델 계열: 스태킹(Stacking)

배깅(Bagging, Bootstrap Aggregating)

배깅은 학습 데이터의 서로 다른 부트스트랩 샘플(Bootstrap Sample)로 각 모델을 학습시켜 다양성을 만듭니다.

flowchart TD
    D[학습 데이터] --> B1[부트스트랩 샘플 1]
    D --> B2[부트스트랩 샘플 2]
    D --> B3[부트스트랩 샘플 3]
    D --> BN[부트스트랩 샘플 N]

    B1 --> M1[모델 1]
    B2 --> M2[모델 2]
    B3 --> M3[모델 3]
    BN --> MN[모델 N]

    M1 --> V[평균 또는 다수결]
    M2 --> V
    M3 --> V
    MN --> V

    V --> P[최종 예측]

부트스트랩 샘플은 원본 데이터에서 복원추출로 뽑으며, 크기는 원본과 같습니다. 각 부트스트랩에는 고유 샘플의 약 63.2%가 등장합니다. 남은 36.8%는 OOB(out-of-bag) 샘플이며, 별도 검증 세트처럼 사용할 수 있습니다.

배깅은 편향을 크게 늘리지 않고 분산을 줄입니다. 각 트리는 자기 부트스트랩 샘플에 과대적합되지만, 과대적합의 방향이 트리마다 다르므로 평균을 내면 잡음이 상쇄됩니다.

랜덤 포레스트(Random Forest) 는 배깅에 한 가지를 더합니다. 각 분할(split)에서 전체 특성이 아니라 무작위 특성 부분집합만 고려합니다. 이렇게 하면 트리 사이의 다양성이 더 커집니다. 일반적으로 분류에서는 sqrt(n_features), 회귀에서는 n_features / 3개의 후보 특성을 사용합니다.

부스팅(Boosting, Sequential Error Correction)

부스팅은 모델을 순차적으로 학습합니다. 새 모델은 이전 모델들이 틀린 예시에 더 집중합니다.

flowchart LR
    D[가중치가 있는 데이터] --> M1[모델 1]
    M1 --> E1[오류 찾기]
    E1 --> W1[오류 예시의 가중치 증가]
    W1 --> M2[모델 2]
    M2 --> E2[오류 찾기]
    E2 --> W2[오류 예시의 가중치 증가]
    W2 --> M3[모델 3]
    M3 --> F[모든 모델의 가중합]

부스팅은 편향을 줄입니다. 새 모델은 지금까지의 앙상블이 가진 체계적 오류를 수정합니다. 최종 예측은 모든 모델의 가중합이며, 성능이 좋은 모델일수록 더 큰 가중치를 받습니다.

단점도 있습니다. 너무 많은 라운드(round)를 돌리면 어려운 예시, 그중 일부는 잡음일 수도 있는 예시에 계속 맞추기 때문에 과대적합될 수 있습니다.

AdaBoost

AdaBoost(Adaptive Boosting)는 처음으로 실용화된 부스팅 알고리즘입니다. 어떤 기본 학습기(base learner)와도 쓸 수 있지만, 보통 결정 그루터기(Decision Stump, 깊이 1 트리)를 사용합니다.

알고리즘은 다음과 같습니다.

1. 샘플 가중치 초기화: 모든 i에 대해 w_i = 1/N

2. t = 1부터 T까지 반복:
   a. 가중 데이터로 약한 학습기 h_t 학습
   b. 가중 오류 계산:
      err_t = sum(w_i * I(h_t(x_i) != y_i)) / sum(w_i)
   c. 모델 가중치 계산:
      alpha_t = 0.5 * ln((1 - err_t) / err_t)
   d. 샘플 가중치 갱신:
      w_i = w_i * exp(-alpha_t * y_i * h_t(x_i))
   e. 가중치 합이 1이 되도록 정규화

3. 최종 예측: H(x) = sign(sum(alpha_t * h_t(x)))

오류가 낮은 모델은 더 큰 alpha를 받습니다. 잘못 분류된 샘플은 더 높은 가중치를 받아 다음 모델이 그 예시에 집중하게 됩니다.

그래디언트 부스팅(Gradient Boosting)

그래디언트 부스팅은 임의의 손실 함수(loss function)에 부스팅을 일반화합니다. 샘플 가중치를 다시 주는 대신, 현재 앙상블의 잔차(residual), 즉 손실의 음의 그래디언트(negative gradient)에 새 모델을 맞춥니다.

1. 초기화: F_0(x) = argmin_c sum(L(y_i, c))

2. t = 1부터 T까지 반복:
   a. 의사 잔차(pseudo-residual) 계산:
      r_i = -dL(y_i, F_{t-1}(x_i)) / dF_{t-1}(x_i)
   b. 잔차 r_i에 트리 h_t 학습
   c. 최적 스텝 크기(step size) 찾기:
      gamma_t = argmin_gamma sum(L(y_i, F_{t-1}(x_i) + gamma * h_t(x_i)))
   d. 갱신:
      F_t(x) = F_{t-1}(x) + learning_rate * gamma_t * h_t(x)

3. 최종 예측: F_T(x)

제곱 오차 손실에서는 의사 잔차가 실제 잔차와 같습니다. r_i = y_i - F_{t-1}(x_i)입니다. 각 트리는 말 그대로 이전 앙상블의 오류를 맞춥니다.

학습률(learning rate, shrinkage)은 각 트리가 얼마나 기여할지 조절합니다. 작은 학습률은 더 많은 트리를 필요로 하지만 일반화가 더 좋아지는 경우가 많습니다. 보통 0.01부터 0.3 사이 값을 씁니다.

XGBoost가 표 형식 데이터를 지배하는 이유

XGBoost(eXtreme Gradient Boosting)는 그래디언트 부스팅에 빠르고 정확하며 과대적합에 강한 엔지니어링 최적화를 더한 것입니다.

  • 정규화된 목적 함수(Regularized Objective): 잎 가중치(leaf weight)에 L1, L2 페널티(penalty)를 적용해 개별 트리가 지나치게 확신하지 않도록 합니다.
  • 2차 근사(Second-order Approximation): 손실의 1차/2차 도함수를 모두 사용해 더 나은 분할(split) 결정을 합니다.
  • 희소성 인식 분할(Sparsity-aware Splits): 결측값을 어느 방향으로 보내는 것이 좋은지 학습해 결측값을 기본 지원합니다.
  • 열 부분표본 추출(Column Subsampling): 랜덤 포레스트처럼 분할마다 특성을 샘플링해 다양성을 높입니다.
  • 가중 분위수 스케치(Weighted Quantile Sketch): 분산 데이터에서 연속 특성의 분할점(split point)을 효율적으로 찾습니다.
  • 캐시 친화적 블록 구조(Cache-aware Block Structure): CPU 캐시 라인(cache line)에 맞춰 메모리 레이아웃을 최적화합니다.

표 형식 데이터에서는 XGBoost와 후속 계열인 LightGBM이 신경망보다 꾸준히 좋은 성능을 내는 경우가 많습니다. 데이터가 행과 열로 표현되는 문제라면 그래디언트 부스팅부터 시작하는 것이 안전합니다.

스태킹(Stacking, Meta-Learning)

스태킹은 여러 기본 모델(base model)의 예측값을 메타 학습기(meta-learner)의 입력 특성으로 사용합니다.

flowchart TD
    D[학습 데이터] --> M1[모델 1: 랜덤 포레스트]
    D --> M2[모델 2: 서포트 벡터 머신]
    D --> M3[모델 3: 로지스틱 회귀]

    M1 --> P1[예측 1]
    M2 --> P2[예측 2]
    M3 --> P3[예측 3]

    P1 --> META[메타 학습기]
    P2 --> META
    P3 --> META

    META --> F[최종 예측]

메타 학습기는 어떤 입력에서 어떤 기본 모델을 신뢰해야 하는지 학습합니다. 특정 영역에서는 랜덤 포레스트가 더 좋고 다른 영역에서는 서포트 벡터 머신(SVM)이 더 좋다면, 메타 학습기가 그 라우팅을 학습합니다.

데이터 누수(data leakage)를 피하려면 기본 모델의 예측은 반드시 학습 세트에서 교차 검증(cross-validation)으로 생성해야 합니다. 같은 데이터로 기본 모델을 학습하고 곧바로 메타 특성을 만들면 안 됩니다.

투표(Voting)

가장 단순한 앙상블입니다. 예측을 직접 결합합니다.

  • 하드 보팅(Hard voting): 클래스 레이블(class label)에 대한 다수결입니다.
  • 소프트 보팅(Soft voting): 예측 확률을 평균낸 뒤 평균 확률이 가장 높은 클래스를 선택합니다. 보통 신뢰도(confidence) 정보를 활용하기 때문에 더 좋습니다.

만들어 보기

Step 1: 결정 그루터기(Decision Stump, Base Learner)

code/ensembles.py는 모든 것을 처음부터 구현합니다. 먼저 단일 분할(split)만 가진 트리인 결정 그루터기부터 시작합니다.

class DecisionStump:
    def __init__(self):
        self.feature_idx = None
        self.threshold = None
        self.polarity = 1
        self.alpha = None

    def fit(self, X, y, weights):
        n_samples, n_features = X.shape
        best_error = float("inf")

        for f in range(n_features):
            thresholds = np.unique(X[:, f])
            for thresh in thresholds:
                for polarity in [1, -1]:
                    pred = np.ones(n_samples)
                    pred[polarity * X[:, f] < polarity * thresh] = -1
                    error = np.sum(weights[pred != y])
                    if error < best_error:
                        best_error = error
                        self.feature_idx = f
                        self.threshold = thresh
                        self.polarity = polarity

    def predict(self, X):
        n = X.shape[0]
        pred = np.ones(n)
        idx = self.polarity * X[:, self.feature_idx] < self.polarity * self.threshold
        pred[idx] = -1
        return pred

Step 2: AdaBoost 처음부터 구현하기

class AdaBoostScratch:
    def __init__(self, n_estimators=50):
        self.n_estimators = n_estimators
        self.stumps = []
        self.alphas = []

    def fit(self, X, y):
        n = X.shape[0]
        weights = np.full(n, 1 / n)

        for _ in range(self.n_estimators):
            stump = DecisionStump()
            stump.fit(X, y, weights)
            pred = stump.predict(X)

            err = np.sum(weights[pred != y])
            err = np.clip(err, 1e-10, 1 - 1e-10)

            alpha = 0.5 * np.log((1 - err) / err)
            weights *= np.exp(-alpha * y * pred)
            weights /= weights.sum()

            stump.alpha = alpha
            self.stumps.append(stump)
            self.alphas.append(alpha)

    def predict(self, X):
        total = sum(a * s.predict(X) for a, s in zip(self.alphas, self.stumps))
        return np.sign(total)

Step 3: 그래디언트 부스팅 처음부터 구현하기

class GradientBoostingScratch:
    def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3):
        self.n_estimators = n_estimators
        self.lr = learning_rate
        self.max_depth = max_depth
        self.trees = []
        self.initial_pred = None

    def fit(self, X, y):
        self.initial_pred = np.mean(y)
        current_pred = np.full(len(y), self.initial_pred)

        for _ in range(self.n_estimators):
            residuals = y - current_pred
            tree = SimpleRegressionTree(max_depth=self.max_depth)
            tree.fit(X, residuals)
            update = tree.predict(X)
            current_pred += self.lr * update
            self.trees.append(tree)

    def predict(self, X):
        pred = np.full(X.shape[0], self.initial_pred)
        for tree in self.trees:
            pred += self.lr * tree.predict(X)
        return pred

Step 4: sklearn과 비교하기

코드는 처음부터 구현한 결과가 sklearn의 AdaBoostClassifier, GradientBoostingClassifier와 비슷한 정확도를 내는지 확인하고, 모든 방법을 나란히 비교합니다.

사용하기

어떤 방법을 언제 쓸까

방법줄이는 항목적합한 경우주의할 점
배깅 / 랜덤 포레스트분산잡음이 있는 데이터, 특성이 많은 데이터편향 문제에는 도움이 되지 않음
AdaBoost편향깨끗한 데이터, 단순한 기본 학습기이상치와 잡음에 민감함
그래디언트 부스팅편향표 형식 데이터, 대회학습이 느리고 튜닝 없이 오래 돌리면 과대적합되기 쉬움
XGBoost / LightGBM둘 다운영용 표 형식 ML하이퍼파라미터가 많음
스태킹둘 다마지막 1-2% 정확도 개선복잡하고 메타 학습기 과대적합 위험이 있음
투표분산다양한 모델 2-3개를 빠르게 결합모델들이 다양할 때만 도움됨

표 형식 데이터를 위한 운영 스택

대부분의 표 형식 예측 문제에서는 다음 순서로 시도합니다.

  1. 기본 파라미터의 LightGBM 또는 XGBoost
  2. n_estimators, learning_rate, max_depth, min_child_weight 튜닝
  3. 마지막 0.5%가 필요하면 서로 다른 모델 3-5개로 스태킹 앙상블 구성
  4. 전체 과정에서 교차 검증(cross-validation) 사용

표 형식 데이터에서 신경망은 계속 연구되고 있지만, 잘 튜닝한 그래디언트 부스팅보다 약한 경우가 많습니다. TabNet, NODE 같은 구조가 가끔 비슷한 성능을 내지만, 잘 튜닝한 XGBoost를 자주 이기지는 못합니다.

산출물 만들기

이 lesson의 최종 산출물은 outputs/prompt-ensemble-selector.mdoutputs/skill-ensemble-builder.md입니다.

  • prompt-ensemble-selector.md에는 데이터 크기, 특성 유형, 잡음 수준, 클래스 균형(class balance), 해결하려는 문제를 넣어 적절한 앙상블 방법을 추천받습니다.
  • skill-ensemble-builder.md에는 앙상블 방법을 고르고 초기 설정을 정할 때 참고할 수 있는 선택 가이드가 들어 있습니다.
  • 산출물은 방법 추천, 시작 하이퍼파라미터, 흔한 실수 경고가 서로 일관되는지 검수할 수 있어야 합니다.

연습문제

  1. AdaBoost 구현을 수정해 각 라운드(round) 이후의 학습 정확도를 기록해봅니다. 추정기(estimator) 수에 따른 정확도를 그립니다. 언제 수렴하나요?

  2. 회귀 트리에 무작위 특성 부분추출(random feature subsampling)을 추가해 랜덤 포레스트를 처음부터 구현합니다. max_features=sqrt(n_features)로 트리 100개를 학습하고 예측을 평균냅니다. 단일 트리와 비교해 분산 감소를 확인합니다.

  3. 그래디언트 부스팅 구현에 조기 종료(early stopping)를 추가합니다. 각 라운드 이후 검증 손실을 추적하고, 10번 연속 개선되지 않으면 멈춥니다. 실제로 몇 개의 트리가 필요했나요?

  4. 로지스틱 회귀(Logistic Regression), 결정 트리(Decision Tree), K-최근접 이웃(K-Nearest Neighbors) 세 기본 모델과 로지스틱 회귀 메타 학습기로 스태킹 앙상블을 만듭니다. 5-겹 교차 검증(5-fold cross-validation)으로 메타 특성(meta-feature)을 만들고 각 기본 모델 단독 성능과 비교합니다.

  5. 같은 데이터셋에서 기본 파라미터로 XGBoost를 실행합니다. 처음부터 구현한 그래디언트 부스팅과 정확도, 실행 시간을 비교합니다. 속도 차이는 얼마나 큰가요?

핵심 용어

용어흔한 설명실제 의미
배깅(Bagging)"무작위 부분집합으로 학습"부트스트랩 샘플로 모델을 학습하고 예측을 평균내 분산을 줄이는 부트스트랩 집계(bootstrap aggregating)
부스팅(Boosting)"어려운 예시에 집중"모델을 순차적으로 학습해 지금까지의 앙상블 오류를 고치고 편향을 줄이는 방법
AdaBoost"데이터 가중치 조정"잘못 분류된 샘플의 가중치를 높여 다음 학습기가 집중하게 하는 부스팅
그래디언트 부스팅(Gradient Boosting)"잔차를 맞춤"새 모델을 손실 함수의 음의 그래디언트에 맞추는 부스팅
XGBoost"Kaggle 무기"정규화, 2차 최적화, 시스템 수준 속도 개선을 더한 그래디언트 부스팅
스태킹(Stacking)"모델 위의 모델"기본 모델의 예측을 메타 학습기의 입력 특성으로 사용하는 방법
랜덤 포레스트(Random Forest)"무작위 트리 여러 개"결정 트리 배깅에 분할(split)별 무작위 특성 부분추출(random feature subsampling)을 더한 앙상블
앙상블 다양성(Ensemble Diversity)"서로 다른 실수 만들기"개별 모델보다 앙상블이 좋아지려면 모델 오류의 상관이 낮아야 한다는 조건
OOB 오류(Out-of-bag Error)"공짜 검증"부트스트랩에 뽑히지 않은 샘플 약 36.8%를 검증 세트처럼 쓰는 방식

더 읽을거리

실습 코드

이 강의의 실습 코드 1개

ensembles
Code

산출물

이 강의에서 생성된 프롬프트, 스킬, 코드 산출물 2개

skill-ensemble-builder

Choose the right ensemble method and configure it for your problem

Skill
prompt-ensemble-selector

Pick the right ensemble method for a given dataset and problem

Prompt

확인 문제

3문제 · 모두 맞추면 완료 표시가 가능합니다

1.AdaBoost에서 잘못 분류된 학습 포인트의 샘플 가중치는 각 라운드(round) 이후 어떻게 되나요?

2.트리 100개의 랜덤 포레스트와 트리 200개의 랜덤 포레스트가 같은 테스트 정확도를 보입니다. 500개로 늘려도 개선이 없습니다. 이유는 무엇인가요?

3.그래디언트 부스팅은 각 새 트리를 어떤 값에 맞추어 학습하나요?

0/3 답변 완료