한 사람의 판단보다는 여러 사람의 판단을 모으면 더 안정적인 결론을 얻을 때가 있다. 머신러닝에서도 하나의 모델만 사용하는 대신 여러 모델의 예측을 결합하는 방법을 사용한다.
이 접근을 앙상블(Ensemble)이라고 한다. 대표적인 기법인 Bagging과 Boosting의 차이를 이해하면 Random Forest, Gradient Boosting 같은 모델을 쉽게 구분할 수 있다.
결정트리 하나는 학습 데이터의 작은 변화에도 나무 구조가 크게 달라질 수 있다. 이런 모델을 여러 개 만든 다음 예측을 통합하면 개별 모델의 우연한 흔들림을 줄일 수 있다.
단, 여러 모델을 합치면 무조건 좋은 성능이 보장되는 것은 아니다. 모델 간 오류의 상관관계, 데이터 특성, 하이퍼파라미터가 모두 영향을 준다.
Bagging(Bootstrap Aggregating)은 데이터를 복원추출한 서로 다른 표본으로 여러 모델을 학습시킨 뒤, 그 예측을 모으는 방법이다.
원본 학습 데이터
├── 복원추출 표본 A → 모델 A ─┐
├── 복원추출 표본 B → 모델 B ─┼→ 예측 집계
└── 복원추출 표본 C → 모델 C ─┘
복원추출은 한 데이터가 한 표본에 여러 번 등장할 수 있다는 뜻이다.
Random Forest는 여러 결정트리를 결합하는 대표적인 Bagging 계열 모델이다.
특히 다음 두 요소가 핵심이다.
이렇게 만들면 모든 트리가 항상 똑같은 질문만 반복하는 현상을 줄이고, 다양한 판단을 모을 수 있다.
가상의 고객 지원 요청 데이터를 만들어 "사람의 추가 검토가 필요한가"를 분류해 보자.
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split
rng = np.random.default_rng(42)
n = 700
wait_minutes = rng.uniform(0, 90, n)
reopen_count = rng.integers(0, 6, n)
priority_score = rng.integers(1, 6, n)
text_length = rng.integers(20, 500, n)
risk = (
-3.0
+ 0.025 * wait_minutes
+ 0.55 * reopen_count
+ 0.3 * priority_score
+ 0.001 * text_length
)
prob = 1 / (1 + np.exp(-risk))
need_review = rng.binomial(1, prob)
X = pd.DataFrame({
"wait_minutes": wait_minutes,
"reopen_count": reopen_count,
"priority_score": priority_score,
"text_length": text_length,
})
y = need_review
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, random_state=42, stratify=y
)
model = RandomForestClassifier(
n_estimators=200,
max_depth=7,
min_samples_leaf=3,
random_state=42,
n_jobs=-1,
)
model.fit(X_train, y_train)
pred = model.predict(X_test)
print(classification_report(y_test, pred, zero_division=0))
n_estimators=200은 트리 200개를 사용한다는 뜻이다. 많이 늘리면 성능이 안정되는 경우가 있지만 계산 비용도 커진다.
| 이름 | 역할 |
|---|---|
n_estimators | 트리의 개수 |
max_depth | 개별 트리 최대 깊이 |
min_samples_leaf | 리프에 필요한 최소 표본 수 |
max_features | 분할마다 고려하는 변수 수 또는 비율 |
bootstrap | 복원추출 사용 여부 |
class_weight | 클래스 가중치 설정 |
n_jobs | 병렬 처리 작업 수 |
트리 개수를 늘리는 것 외에도 개별 트리의 복잡도를 제어해야 한다.
importance = pd.Series(
model.feature_importances_,
index=X.columns,
).sort_values(ascending=False)
print(importance)
Random Forest의 feature_importances_는 일반적으로 불순도 감소량에 기반한다. 따라서 값이 크다고 원인이라고 판단하면 안 되며, 범주가 많거나 연속적인 변수에 유리한 편향이 나타날 수도 있다.
검증 데이터에서 특정 변수만 섞은 뒤 예측 성능이 얼마나 떨어지는지 측정하는 permutation_importance()로 교차 확인할 수 있다.
from sklearn.inspection import permutation_importance
result = permutation_importance(
model, X_test, y_test,
n_repeats=5,
random_state=42,
scoring="f1",
)
print(pd.Series(result.importances_mean, index=X.columns).sort_values(ascending=False))
Bootstrap 표본에 포함되지 않은 학습 데이터는 해당 트리의 예측을 검사하는 데 활용할 수 있다.
oob_model = RandomForestClassifier(
n_estimators=200,
oob_score=True,
bootstrap=True,
random_state=42,
)
oob_model.fit(X_train, y_train)
print("OOB score:", oob_model.oob_score_)
OOB는 별도 검증 점수에 대한 보조 정보가 될 수 있지만, 최종 테스트 데이터 평가를 대체하는 만능 지표는 아니다.
Boosting은 앞선 모델의 부족한 부분을 다음 모델이 보완해 가도록 모델을 순차적으로 결합하는 접근이다.
모델 1 → 남은 오차 또는 어려운 사례 확인
↓
모델 2
↓
모델 3 → 최종 결합
대표적으로 Gradient Boosting은 손실 함수를 낮추는 방향으로 새로운 모델을 순차적으로 추가한다.
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import f1_score
boosting = GradientBoostingClassifier(random_state=42)
boosting.fit(X_train, y_train)
print("Random Forest F1:", f1_score(y_test, model.predict(X_test)))
print("Gradient Boosting F1:", f1_score(y_test, boosting.predict(X_test)))
어느 쪽이 더 나은지는 데이터와 튜닝에 따라 달라진다. 위 코드는 동일한 데이터로 동작 차이를 체험하는 예시이며, 실제 모델 선택은 교차검증으로 수행하는 것이 적절하다.
XGBoost는 Gradient Boosting을 효율적으로 구현한 별도 라이브러리다. Scikit-learn에 기본 포함되어 있지 않으므로 사용할 때는 별도 설치가 필요하다.
pip install xgboost
단순히 RandomForestClassifier를 XGBClassifier로 바꾸기만 하면 항상 결과가 좋아지는 것은 아니다. 버전 호환성, 입력 데이터 처리, 평가 방식, 하이퍼파라미터를 함께 살펴봐야 한다.
우선 간단한 기준 모델을 만들고, 같은 검증 방식으로 비교하는 것이 중요하다.
n_estimators는 트리 수, max_depth는 개별 트리의 복잡도를 제어한다.