AdaBoost 밑바닥부터 구현하기: 멍청한 규칙들의 모임이 어떻게 똑똑한 분류기가 되는가
요약
AdaBoost 알고리즘의 수학적 원리를 밑바닥부터 구현하며 설명하는 기술 가이드입니다. 결정 스텀프와 샘플 가중치 업데이트 방식을 통해 약한 학습기가 어떻게 강력한 분류기로 진화하는지 다룹니다.
핵심 포인트
- 결정 스텀프를 활용한 약한 학습기(Weak Learner)의 개념 이해
- 데이터 샘플 가중치 조절을 통한 오차 최소화 메커니즘
- 가중치 적용된 오차(Weighted Error)를 통한 최적의 스텀프 선택 방식
- 직접 구현한 코드를 통한 알고리즘의 실질적 작동 원리 파악
마치 함정 질문처럼 들리는 질문이 하나 있습니다. 동전 던지기보다 겨우 나은 수준의 모델들로 정확한 분류기 (classifier)를 만들 수 있을까요?
놀랍게도, 가능합니다. 이것이 바로 부스팅 (boosting)의 핵심 아이디어이며, AdaBoost는 이를 유명하게 만든 알고리즘입니다. 저는 이를 밑바닥부터 직접 구현하여 인터랙티브 데모에 담았습니다. 여기 실제 수학적 원리를 바탕으로, 모호한 설명 없이 작동 방식을 설명합니다.
라이브 버전 체험하기: https://dev48v.infy.uk/ml/day21-adaboost.html
약한 학습기 (weak learner): 결정 스텀프 (decision stump)
AdaBoost의 구성 요소는 상상할 수 있는 가장 단순한 분류기인 **결정 스텀프 (decision stump)**입니다. 이는 정확히 하나의 분기 (split)만을 가진 결정 트리 (decision tree)입니다. 하나의 특성 (feature)을 보고, 이를 하나의 임계값 (threshold)과 비교한 뒤, 한쪽은 "+1", 반대쪽은 "−1"이라고 부르는 것입니다. 그게 전부입니다. 한 줄, 한 번의 절단.
def stump_predict(X, dim, thresh, polarity):
pred = np.ones(len(X))
if polarity == 1:
...
자명하게 분리되지 않는 어떤 데이터에 대해서도, 단일 스텀프는 가망이 없습니다. 체커보드 (checkerboard) 레이아웃에서는 겨우 55-60% 정도의 정확도를 보입니다. 이것이 바로 이것이 "약한 학습기 (weak learner)"인 정확한 이유입니다. 즉, 무작위 추측보다 아주 근소하게 나은 수준의 모델을 의미합니다. 마법은 이 수백 개의 스텀프를 어떻게 결합하느냐에 달려 있습니다.
샘플 가중치 (Sample weights): 움직이는 스포트라이트
AdaBoost의 엔진은 모든 훈련 데이터 포인트에 부여되는 가중치로, "이 데이터를 맞히는 것이 얼마나 중요한가?"를 나타냅니다. 모든 것은 동일하게 시작합니다:
n = len(X)
w = np.full(n, 1.0 / n) # 균등 분포 (uniform): 모든 포인트의 가중치는 1/n
이 가중치들은 확률 분포 (probability distribution)이며, 합계는 1이 됩니다. 매 라운드가 끝날 때마다 가중치는 변합니다. 우리가 맞힌 포인트는 가중치가 가벼워지고, 틀린 포인트는 가중치가 무거워집니다. 우리는 항상 가중치 적용된 (weighted) 오차를 최소화하도록 다음 스텀프를 선택하기 때문에, 무거운 포인트들이 결국 탐색 과정을 지배하게 됩니다. 다음 스텀프는 사실상 위원회가 계속해서 실수하는 부분을 뚫어지게 쳐다보도록 강제됩니다.
단순한 개수가 아닌 가중치 적용된 오차 (Weighted error)
매 라운드마다 최적의 스텀프를 찾을 때, 우리는 단순히 실수 횟수를 세는 것이 아니라, 실수의 *가중치 (weight)*를 모두 더합니다:
def weighted_error(pred, y, w):
return w[pred != y].sum() # 실수 횟수가 아닌, 실수의 가중치 (weight) 합
초기 단계에서는 가중치가 균등하기 때문에, 이는 일반적인 오차율 (error rate)과 같습니다. 하지만 일부 데이터 포인트에 높은 가중치가 부여되면, 가벼운 오차를 몇 개 범하더라도 가중치가 높은 포인트들을 정확히 맞춘 스텀프 (stump)가 낮은 가중 오차 (weighted error)를 기록하게 됩니다. 따라서 "최적의 스텀프"는 매 라운드마다 현재의 어려운 사례들 쪽으로 조용히 이동하며, 우리는 어떤 포인트가 어려운지 직접 알려줄 필요가 없습니다. 가중치가 우리 대신 그 역할을 수행하기 때문입니다.
알파 (alpha) 공식: 왜 로그 (logarithm)인가?
스텀프의 가중 오차를 알게 되면, 최종 앙상블 (ensemble)에서 해당 스텀프의 투표권이 얼마나 강력할지를 결정합니다:
eps = 1e-10
err = min(max(err, eps), 1 - eps) # 로그 계산을 위한 보호 장치
alpha = 0.5 * np.log((1 - err) / err)
이 공식의 형태를 유심히 살펴보세요. 모든 구성 요소가 제 자리를 찾아가고 있습니다:
err → 0일 때, 비율(1-err)/err은 폭발적으로 증가하며alpha → +∞가 됩니다. 거의 완벽한 스텀프가 지배적인 영향력을 갖게 됩니다.err = 0.5일 때, 비율은 1이 되고ln(1) = 0이 되므로, 동전 던지기 수준의 스텀프는alpha = 0을 얻어 아무런 발언권도 갖지 못합니다.err > 0.5일 때, 로그 값은 음수가 됩니다. 따라서 무작위 추측보다 못한 스텀프는 **음수의 알파 (negative alpha)**를 받게 되며, 그 투표 결과는 단순히 반대로 뒤집힙니다.
로그는 단순히 장식용이 아닙니다. 이는 AdaBoost가 비밀리에 경사 하강법 (gradient descent)을 수행하고 있는 지수 손실 (exponential loss)을 최소화하는 정확한 값입니다. 이것이 바로 부스팅 (boosting)이 훈련 오차 (training error)를 낮춘다는 것을 증명할 수 있는 이유입니다.
재가중치 부여 (Reweighting): 실수를 키우고, 정규화하기
이제 다음 스텀프가 더 어려운 문제에 직면하도록 가중치를 재구성합니다:
pred = stump_predict(X, dim, thresh, polarity)
w = w * np.exp(-alpha * y * pred) # 맞으면 줄어들고, 틀리면 exp(alpha)만큼 증가
w = w / w.sum() # 합이 다시 1이 되도록 정규화 (renormalise)
스텀프가 정답을 맞히면 y * pred = +1이 되어 지수가 음수가 되고 가중치는 줄어듭니다. 틀렸을 때는 가중치가 정확히 exp(alpha)만큼 증가합니다. 즉, 확신에 찬 스텀프일수록 재가중치를 더 강하게 부여합니다. 그 후 전체 합으로 나누어 가중치의 총합이 다시 1이 되도록 하여, 유효한 분포 (distribution)로 만듭니다.
저는 이 과정을 수치적으로 검증했습니다. 매 라운드마다 재정규화된 가중치(renormalised weights)의 합은 소수점 열째 자리까지 1.0이 되며, 알파(alpha) 값은 공식과 정확히 일치합니다 (오차=0.5일 때 0.0, 오차=0.1일 때 1.099, 오차=0.01일 때 2.298). 데모에서 오분류된 지점들이 라운드가 거듭될수록 눈에 띄게 '부풀어 오르는(swell)' 이유가 바로 이것입니다.
강력한 분류기: 가중 투표
최종 모델은 단순한 다수결 투표가 아닙니다. 가중치가 부여된 투표입니다:
def predict(ensemble, X):
total = np.zeros(len(X))
for alpha, dim, thr, pol in ensemble:
...
모든 스텀프(stump)에게 ±1 답변을 요구하고, 각 답변에 해당 알파(alpha)를 곱한 뒤, 모두 더하여 부호(sign)를 취합니다. 확신이 있는 스텀프는 합계에 큰 영향을 미치고, 약한 스텀프는 거의 영향을 주지 못합니다. 공식적으로는 F(x) = sign(Σ αₜ·hₜ(x)) — 즉, 가산 모델 (additive model)입니다. 제 데모에서 블록 형태의 음영 처리된 배경은 전체 평면에 대해 계산된 바로 이 합계의 부호입니다. XOR 스타일의 데이터에서 저는 이것이 25라운드에 걸쳐 훈련 정확도 60%에서 85%까지 상승하는 것을 관찰했습니다. 이때 각 개별 스텀프는 내내 40% 근처의 오차에 머물러 있었습니다. 이것이 바로 보상입니다. 단일 학습자는 개선되지 않았지만, 위원회(committee)는 개선되었습니다.
부스팅은 편향(bias)을 줄인다
랜덤 포레스트 (random forests)와 대조해 보십시오. 배깅 (Bagging)은 편향이 낮은 많은 강력한 트리들을 평균 내어 분산 (variance)을 줄입니다. 부스팅 (Boosting)은 그 반대입니다. 심각하게 과소적합 (underfit)되는 편향이 높은 스텀프들로 시작하여, 전체의 잔차 (residual)를 하나씩 수정하며 이를 하나씩 추가합니다. 따라서 앙상블의 편향은 꾸준히 감소하며, 경계면은 매 라운드마다 더 표현력이 풍부해집니다. 부스팅은 과소적합된 모델을 유연한 모델로 바꾸어 놓습니다 — 이것이 부스팅의 특징입니다.
언제 멈출 것인가
부스팅은 과하게 진행될 수 있습니다. 충분한 라운드를 거치면 훈련 오차 (training error)를 0으로 만들 수 있지만, 특정 지점을 지나면 AdaBoost는 노이즈에 적합 (fitting)하기 시작하며 테스트 오차 (test error)가 다시 올라갑니다. 어려운 지점에 높은 가중치를 부여하기 때문에, AdaBoost는 잘못 레이블링된 예시나 이상치 (outliers)에 특히 민감합니다. 즉, 결코 이길 수 없는 지점에 계속해서 배팅을 거듭하게 됩니다. 해결책은 일반적인 방법들과 같습니다: 라운드 수를 제한하고, 학습률 (learning rate)을 사용하여 각 알파를 줄이며, 교차 검증 (cross-validation)을 통해 이 두 가지를 모두 선택하는 것입니다.
실무에서는
실제 운영 환경(production)에서 이를 직접 구현하는 일은 결코 없을 것입니다. Scikit-learn은 이를 하나의 객체로 제공합니다:
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
...
max_depth=1은 정확히 우리가 만든 스텀프 (stump)입니다. n_estimators는 라운드 (rounds)의 횟수입니다. learning_rate는 과적합 (overfitting)을 방지하는 알파 축소 (alpha-shrinkage)입니다. 모든 것이 방금 우리가 직접 구축한 루프 (loop)와 그대로 매핑됩니다.
스텀프를 사용하는 AdaBoost는 실시간 얼굴 탐지를 가능하게 했던 고전적인 Viola-Jones 얼굴 탐지기 (face detector)의 핵심 동력이었습니다. 그 이후로는 그래디언트 부스팅 (Gradient boosting; XGBoost, LightGBM)이 주도권을 가져갔지만, AdaBoost는 여전히 부스팅 (boosting)을 _이해하기_에 가장 명확한 방법입니다: 가중치 재설정 (reweight), 재학습 (refit), 재투표 (revote).
라운드 슬라이더를 드래그하며 실시간으로 일어나는 과정을 확인해 보세요: https://dev48v.infy.uk/ml/day21-adaboost.html
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기