머신러닝 기초 (11) - 앙상블: 랜덤포레스트와 부스팅
트리 하나의 높은 분산을 여러 트리로 다루는 두 방식, 독립적으로 만들어 평균하는 배깅(랜덤포레스트)과 순차적으로 오차를 보정하는 부스팅(GBM)이 각각 무엇을 줄이는지, 어떻게 다른지를 load_breast_cancer로 비교하는 11편입니다.
머신러닝 기초 시리즈의 11편입니다. 10편의 “결정트리”에 이어집니다.
트리 하나로는 부족하다
10편에서 결정트리는 데이터를 조건으로 계속 쪼개 예측하는 모델이었습니다. 해석이 쉽고 전처리 요구가 적지만 약점이 뚜렷했습니다. 끝까지 자란 트리는 학습 데이터에 거의 완벽하게 맞추는 대신, 데이터가 조금만 바뀌어도 분할 구조가 통째로 달라집니다. 4편의 용어로 말하면 편향은 낮지만 분산이 높은 모델입니다.
앙상블(ensemble)은 이 문제를 트리 하나를 더 잘 만드는 대신 여러 트리를 묶어 다룹니다. 방향은 둘로 갈립니다.
- 배깅(bagging): 여러 트리를 서로 독립적으로 만들어 예측을 평균한다. 목표는 분산을 줄이는 것.
- 부스팅(boosting): 트리를 하나씩 순차적으로 더하며 앞선 트리가 남긴 오차를 보정한다. 목표는 편향을 줄이는 것.
같은 결정트리를 재료로 쓰지만 겨냥하는 것이 반대입니다. 이 편은 두 방식이 각각 무엇을 어떻게 줄이는지, 그래서 실전에서 어떻게 다르게 행동하는지를 봅니다.
배깅: 독립적으로 만들어 평균한다
배깅은 Bootstrap Aggregating의 줄임말입니다. 이름에 방법이 다 들어 있습니다.
부트스트랩(bootstrap): 학습 데이터에서 중복을 허용해 원본과 같은 크기로 다시 뽑습니다. 트리마다 조금씩 다른 데이터셋을 갖게 됩니다. 중복 추출이라 한 부트스트랩 표본에는 원본의 일부만 들어가는데, 평균적으로 약 63%만 포함되고 나머지 약 37%는 빠집니다.
1
2
3
4
5
6
import numpy as np
rng = np.random.default_rng(42)
n = 100_000
idx = rng.integers(0, n, size=n) # 중복 허용, 같은 크기로 추출
len(np.unique(idx)) / n # 원본의 약 63%만 뽑힌다 = 0.633
애그리게이팅(aggregating): 이렇게 만든 트리 여러 개의 예측을 모읍니다. 회귀면 평균, 분류면 다수결입니다.
왜 평균이 분산을 줄일까요. 서로 독립인 예측값 여러 개를 평균하면 개별 예측의 흔들림이 서로 상쇄됩니다. 완전히 독립인 n개 예측을 평균하면 분산이 1/n로 줄어듭니다. 트리 하나하나는 여전히 과적합한 고분산 모델이지만, 이들을 평균한 결과는 훨씬 안정적입니다. 편향은 거의 그대로 두고 분산만 깎는 것이 배깅의 핵심입니다.
빠진 37%도 버리지 않습니다. 각 트리 입장에서 자기 학습에 쓰이지 않은 이 표본(out-of-bag)으로 트리별 검증을 별도 분할 없이 얻을 수 있습니다. RandomForestClassifier(oob_score=True)가 계산해 주는 값이 이것입니다.
랜덤포레스트: 트리끼리 닮지 않게 만든다
방금 “완전히 독립이면 분산이 1/n로 준다”고 했지만, 부트스트랩 표본들은 원본 하나에서 나왔으니 서로 닮아 있습니다. 트리들이 비슷하면 예측도 비슷하게 틀리고, 평균해도 분산이 잘 줄지 않습니다. 평균으로 분산을 줄이려면 트리들이 서로 달라야 합니다.
랜덤포레스트는 여기에 무작위성을 하나 더 넣습니다. 각 분할에서 전체 특성이 아니라 무작위로 고른 일부 특성만 후보로 둡니다(max_features). 강한 특성 하나가 모든 트리의 뿌리를 똑같이 지배하는 것을 막아, 트리들의 상관을 낮춥니다. 상관이 낮아질수록 평균의 분산이 더 내려갑니다.
정리하면 랜덤포레스트는 부트스트랩(데이터 무작위) + 특성 무작위로 서로 닮지 않은 깊은 트리들을 만들고, 그 예측을 평균하는 모델입니다. 트리를 늘려도 과적합으로 무너지지 않고 성능이 완만하게 수렴합니다. 늘린다고 나빠지지 않으니 계산 여유가 허락하는 만큼 두면 됩니다.
부스팅: 순차적으로 오차를 보정한다
부스팅은 반대에서 출발합니다. 배깅이 고분산 트리를 평균해 분산을 깎았다면, 부스팅은 약한 트리를 계속 보정해 편향을 깎습니다.
여기서 약한 트리란 얕은 트리입니다. 깊이가 얕으면 데이터를 대충만 나누므로 편향이 높고 분산은 낮습니다. 이런 트리 하나는 예측이 형편없지만, 부스팅은 이를 하나씩 이어 붙이며 앞 단계의 오차를 다음 트리가 메우게 합니다.
그래디언트 부스팅(Gradient Boosting)의 절차는 이렇습니다.
- 아주 단순한 예측에서 시작한다(예: 전체 평균).
- 지금까지 모델이 남긴 오차, 즉 잔차를 계산한다.
- 그 잔차를 맞추는 얕은 트리를 하나 학습해 모델에 더한다.
- 2~3을 반복한다. 트리를 더할 때마다 잔차가 줄어든다.
각 트리가 예측이 아니라 “앞선 모델이 틀린 만큼”을 학습한다는 점이 핵심입니다. 제곱오차를 쓰면 이 잔차가 손실 함수의 음의 기울기와 같아서 ‘그래디언트’ 부스팅이라 부릅니다. 3편의 경사하강법이 파라미터를 조금씩 옮겨 손실을 줄였다면, 부스팅은 트리를 하나씩 더해 손실을 줄이는 셈입니다.
한 가지 장치가 더 있습니다. 새 트리를 통째로 더하면 한 번에 너무 많이 움직여 과적합하기 쉽습니다. 그래서 각 트리의 기여를 learning_rate(보통 0.05~0.1)만큼 줄여서 더합니다. 보폭을 줄이는 대신 트리를 더 많이 쌓습니다.
이 구조가 부스팅의 성격을 정합니다.
- 순차적이다: 다음 트리는 이전 트리의 결과에 의존하므로, 트리들을 병렬로 만들 수 없습니다.
- 트리를 늘리면 과적합할 수 있다: 배깅과 달리 트리를 계속 더하면 학습 데이터의 잡음까지 맞추기 시작합니다. 그래서 트리 개수는 검증 성능으로 멈춰야 합니다(5편의 early stopping).
- 튜닝에 민감하다:
learning_rate와 트리 개수, 트리 깊이가 서로 얽혀 있어 손이 더 갑니다.
LightGBM과 XGBoost
그래디언트 부스팅의 아이디어는 오래됐지만, 원래 구현은 느렸습니다. 모든 특성의 모든 분할 지점을 순차적으로 평가해야 했기 때문입니다. XGBoost와 LightGBM은 같은 원리를 빠르게 만든 구현체입니다.
- 히스토그램 기반 분할: 연속값 특성을 몇백 개 구간(bin)으로 미리 묶어 후보 분할점 수를 크게 줄입니다. 정확도는 거의 유지하면서 속도가 수십 배 빨라집니다.
- 결측치와 범주형 처리 내장: 결측치를 별도로 채우지 않아도 트리가 알아서 분기 방향을 정하고, 범주형 특성도 원-핫 없이 다룹니다.
- 규제와 조기 종료: L1/L2 규제, 트리 복잡도 제약, 검증 성능 기반 early stopping을 기본 제공합니다.
둘의 차이는 주로 트리를 키우는 순서입니다. XGBoost는 같은 깊이를 고루 채우는 방식(level-wise)이 기본이고, LightGBM은 손실이 가장 많이 줄어드는 잎을 먼저 키우는 방식(leaf-wise)이라 대체로 더 빠르고 큰 데이터에서 유리합니다. 잘 정리된 테이블 데이터에서는 이 계열(GBDT)이 사실상의 표준 선택입니다.
배깅과 부스팅, 무엇이 다른가
같은 결정트리를 쓰지만 겨냥하는 것과 행동이 반대입니다.
| 구분 | 배깅 (랜덤포레스트) | 부스팅 (GBM) |
|---|---|---|
| 학습 방식 | 병렬, 트리를 독립적으로 | 순차, 이전 오차를 보정 |
| 주로 줄이는 것 | 분산 | 편향 |
| 기반 트리 | 깊은 트리 (저편향, 고분산) | 얕은 트리 (고편향, 저분산) |
| 트리를 늘리면 | 과적합에 견고, 성능 수렴 | 과적합 가능, 멈출 지점 필요 |
| 튜닝 난이도 | 낮음, 대충 둬도 잘 됨 | 높음, learning_rate와 트리 수가 얽힘 |
| 병렬화 | 쉬움 | 트리 사이는 어려움 |
한 문장으로 줄이면, 배깅은 고분산 모델을 평균해 분산을 깎고, 부스팅은 고편향 모델을 이어 붙여 편향을 깎습니다. 잘 튜닝한 부스팅이 성능은 대체로 더 높지만, 손이 덜 가고 실수해도 잘 무너지지 않는 것은 랜덤포레스트입니다. 그래서 랜덤포레스트를 먼저 강한 기준선으로 두고, 부스팅으로 성능을 끌어올리는 순서가 흔합니다.
실습: 한 데이터에서 셋을 비교한다
이진분류용 내장 데이터 load_breast_cancer로 단일 트리, 랜덤포레스트, 그래디언트 부스팅을 같은 조건에서 비교합니다. 세포핵의 30개 측정값으로 종양의 양성/악성을 가리는 문제입니다. 5편의 cross_val_score로 5-겹 교차검증하고, 7편에서 본 ROC-AUC로 채점합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
X, y = load_breast_cancer(return_X_y=True)
models = {
"DecisionTree": DecisionTreeClassifier(random_state=42),
"RandomForest": RandomForestClassifier(n_estimators=300, random_state=42),
"GradientBoosting": GradientBoostingClassifier(random_state=42),
}
for name, model in models.items():
scores = cross_val_score(model, X, y, cv=5, scoring="roc_auc")
print(f"{name:16s} {scores.mean():.4f}")
# DecisionTree 0.9159 ← 트리 하나
# RandomForest 0.9921 ← 배깅
# GradientBoosting 0.9912 ← 부스팅
읽어낼 것이 두 가지입니다.
- 앙상블 둘 다 단일 트리를 크게 앞섭니다. 0.9159에서 0.99대로 뛰었습니다. 트리 하나의 높은 분산을 배깅은 평균으로, 부스팅은 순차 보정으로 각각 눌러 얻은 결과입니다.
- 랜덤포레스트와 부스팅은 여기서 거의 같습니다. 어느 한쪽이 항상 이기는 게 아니라 데이터와 튜닝에 달렸습니다. 중요한 것은 이 결과가 기본 설정이라는 점입니다. 랜덤포레스트는 트리 수만 넉넉히 준 채 거의 손대지 않았고, 부스팅은
learning_rate, 트리 수, 깊이를 제대로 맞추면 여기서 더 올라갈 여지가 큽니다. 대신 그만큼 튜닝 노력이 듭니다.
랜덤포레스트는 튜닝하지 않아도 강한 기준선을 줍니다. 새 문제에서 “이 데이터로 트리 계열이 얼마나 나오나”를 빠르게 보고 싶을 때, 기본값 랜덤포레스트를 먼저 돌려 기준을 잡고 부스팅 튜닝으로 넘어가는 순서가 안전합니다.
NYC 택시 파이프라인이 LightGBM을 고른 이유
이 절의 내용은 NYC 택시 수요 예측 파이프라인에서 실제로 확인한 것입니다. 그 프로젝트는 지역×1시간 단위로 집계한 테이블에서 다음 1시간 승차 건수를 예측하는 문제였고, Baseline, LightGBM, PyTorch MLP를 같은 분할과 지표로 비교했습니다.
결과는 5편 실험 기록에 정리돼 있는데, LightGBM이 최고 Baseline을 MAE 기준 34% 앞섰고 신경망보다도 나았습니다. 이 편의 개념으로 그 선택을 풀면 이렇습니다.
- 잘 정리된 테이블 데이터: 달력 변수, 지연(lag), 이동평균 같은 피처가 이미 표로 정리돼 있었습니다. 이런 형태에서는 GBDT가 적은 튜닝으로 강력하고, 신경망이 역전하려면 더 많은 데이터나 표로 못 담는 구조가 필요합니다.
- 범주형 지역 ID: 지역 식별자를 범주형으로 넣으면 트리가 지역 집합을 자유롭게 분할합니다. 숫자 크기로 오해하지 않게 하는 이 처리를 LightGBM이 기본 지원했습니다.
- 트리 개수를 데이터가 정하게: 부스팅은 트리를 늘리면 과적합할 수 있으므로, 상한만 크게 주고 검증 성능이 개선되지 않으면 멈추는 early stopping으로 개수를 정했습니다. 이 편에서 부스팅의 트리 수를 “검증으로 멈춰야 한다”고 한 그 지점입니다.
즉 LightGBM 선택은 취향이 아니라 문제의 성격에서 따라 나온 결정이었습니다. 테이블 + 범주형 + 빠른 학습과 조기 종료라는 조건이 곧 부스팅 계열, 그중 LightGBM을 가리켰습니다.
정리
| 개념 | 한 줄 요약 |
|---|---|
| 앙상블 | 트리 하나의 높은 분산을 여러 트리를 묶어 다룬다. 방향은 배깅과 부스팅 둘 |
| 배깅 | 부트스트랩으로 뽑은 데이터로 트리를 독립적으로 만들어 예측을 평균. 분산을 줄인다 |
| 랜덤포레스트 | 배깅 + 각 분할에서 특성 무작위 선택. 트리끼리 닮지 않게 해 평균 효과를 키운다 |
| 부스팅 | 얕은 트리를 순차로 더하며 이전 오차(잔차)를 보정. 편향을 줄인다 |
| learning_rate | 각 트리 기여를 줄여 과적합을 늦춘다. 보폭을 줄이고 트리를 더 쌓는다 |
| LightGBM과 XGBoost | GBM의 빠른 구현. 히스토그램 분할, 범주형, early stopping 내장. 테이블 데이터의 표준 |
| 배깅 vs 부스팅 | 배깅은 병렬, 분산 감소, 튜닝 쉬움, 부스팅은 순차, 편향 감소, 튜닝 민감 |
다음 편에서는 지금까지 당연하게 X로 써온 입력을 만드는 과정을 봅니다. 스케일이 제각각인 특성을 맞추고, 범주형을 숫자로 바꾸고, 결측치를 다루는 전처리입니다.