
랜덤 포레스트(Random Forest): 특성 무작위성으로 상관관계를 깨다
- 16랜덤 포레스트(Random Forest): 특성 무작위성으로 상관관계를 깨다읽는 중
- 17부스팅(Boosting): 약한 모델을 순차적으로 쌓아 강한 모델 만들기
- 18XGBoost vs LightGBM: 실전 부스팅 모델, 어떤 걸 써야 할까
- 19인공 신경망(ANN) 기초: 퍼셉트론에서 다층 신경망까지
- 20순전파(Forward Propagation): 신경망이 예측하는 과정
이전 글에서 앙상블과 배깅의 원리를 배웠다. 부트스트랩 샘플링으로 여러 트리를 만들고 다수결로 합치면, 분산이 σ²/B로 줄어든다. 하지만 한 가지 한계가 남았다 — 트리 간 상관관계 ρ. 같은 데이터에서 나온 트리들은 비슷한 구조를 가지기 쉽고, 상관관계가 높으면 Var(T̄) = ρσ² + (1-ρ)σ²/B에서 ρσ² 항이 남아 분산이 더 이상 줄지 않는다.
랜덤 포레스트(Random Forest) 는 이 문제를 해결한다. 각 노드에서 전체 특성 중 일부만 무작위로 선택해서 분기점을 찾는다 — 이 한 가지 아이디어로 트리 간 상관관계를 깨고, 배깅의 분산 한계를 돌파한다.
랜덤 포레스트(Random Forest)
이전 글에서 배운 배깅의 분산 공식을 다시 보자.
배깅 + 특성 무작위성(Feature Randomness)
랜덤 포레스트는 배깅에 특성 무작위성을 추가한다. 각 노드에서 분기점을 찾을 때, 전체 특성 중 일부(max_features개)만 무작위로 선택하고 그 중에서 최선의 분기를 찾는다.
배깅: 각 트리 → 부트스트랩 샘플 사용
랜덤 포레스트: 각 트리 → 부트스트랩 샘플 사용
각 노드 → max_features개 특성만 고려 (← 이 부분이 추가!)이로 인해:
- 강한 특성 하나가 모든 트리를 지배하지 않는다
- 트리마다 서로 다른 특성 조합을 학습한다
- 트리 간 상관관계 ρ가 줄어든다
- 분산 공식
ρσ² + (1-ρ)σ²/B에서 ρ가 작아지므로 전체 분산이 낮아진다
왜 특성 무작위성이 필요한가?
극단적인 예시로 이해해보자. 30개의 특성 중 1개가 압도적으로 중요한 특성이라고 하자. 배깅에서는 모든 트리의 루트 노드가 그 특성을 선택한다. 트리들이 형제처럼 비슷해진다 — ρ ≈ 1.
랜덤 포레스트에서 max_features=5로 설정하면, 어떤 트리는 루트에서 그 특성을 못 보고 다른 특성으로 분기한다. 각 트리가 데이터의 다른 측면을 배운다 — ρ가 크게 줄어든다.
max_features 파라미터
sklearn의 기본값:
- 분류(Classification):
max_features='sqrt'→ √p개 (p = 전체 특성 수) - 회귀(Regression):
max_features=1.0(sklearn 1.1+) 또는'sqrt'
from sklearn.ensemble import RandomForestClassifier
# p=30개 특성이면 sqrt(30) ≈ 5~6개 특성만 각 노드에서 고려
rf = RandomForestClassifier(
n_estimators=100,
max_features='sqrt', # 기본값: 분류에서 √p
random_state=42
)max_features의 직관max_features가 작을수록: 트리 간 상관관계 ↓, 개별 트리 성능 ↓max_features가 클수록: 트리 간 상관관계 ↑, 개별 트리 성능 ↑최적점은 중간 어딘가에 있다. 분류에는 √p, 회귀에는 p/3이 좋은 출발점이다.
sklearn으로 실전 구현
BaggingClassifier vs RandomForestClassifier 비교
이전 글에서 BaggingClassifier의 사용법을 다뤘다. 여기서는 RandomForestClassifier와의 차이를 비교한다.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import BaggingClassifier, RandomForestClassifier
cancer = load_breast_cancer()
X, y = cancer.data, cancer.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 단일 결정 트리
dt = DecisionTreeClassifier(random_state=42)
dt.fit(X_train, y_train)
# 배깅
bagging = BaggingClassifier(
estimator=DecisionTreeClassifier(),
n_estimators=100,
max_samples=1.0, # 부트스트랩 샘플 크기 (기본: 전체)
max_features=1.0, # 특성 서브샘플 없음 (배깅의 경우)
bootstrap=True,
random_state=42,
n_jobs=-1
)
bagging.fit(X_train, y_train)
# 랜덤 포레스트
rf = RandomForestClassifier(
n_estimators=100,
max_features='sqrt', # 특성 무작위성 (핵심 차이!)
random_state=42,
n_jobs=-1
)
rf.fit(X_train, y_train)
print(f"단일 DecisionTree: {dt.score(X_test, y_test):.4f}")
print(f"BaggingClassifier: {bagging.score(X_test, y_test):.4f}")
print(f"RandomForest: {rf.score(X_test, y_test):.4f}")단일 DecisionTree: 0.9474
BaggingClassifier: 0.9561
RandomForest: 0.9649n_estimators에 따른 성능 변화
트리 수가 적을 때는 성능이 불안정하고, 100개 정도에서 수렴한다. 200개 이상은 성능 차이가 미미하지만 학습 시간만 늘어난다. 실전에서는 100~300개가 좋은 출발점이다.
회귀 예시
from sklearn.ensemble import RandomForestRegressor
from sklearn.datasets import fetch_california_housing
from sklearn.metrics import root_mean_squared_error
housing = fetch_california_housing()
X_h, y_h = housing.data, housing.target
X_tr, X_te, y_tr, y_te = train_test_split(X_h, y_h, test_size=0.2, random_state=42)
rf_reg = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1)
rf_reg.fit(X_tr, y_tr)
y_pred = rf_reg.predict(X_te)
rmse = root_mean_squared_error(y_te, y_pred)
print(f"RMSE: {rmse:.4f}")
print(f"R²: {rf_reg.score(X_te, y_te):.4f}")RMSE: 0.5032
R²: 0.8050OOB(Out-of-Bag) 평가
OOB error를 교차 검증 대신 쓸 수 있는 이유
각 트리는 자신의 OOB 샘플(약 36.8%)로 평가할 수 있다. 전체 데이터의 각 샘플은 평균적으로 B × 0.368개 트리의 OOB 샘플이 된다. 그 트리들의 예측만 모아 다수결을 내면 자연스럽게 교차 검증과 유사한 검증이 된다.
교차 검증(k-fold)은 데이터를 k번 다시 학습해야 하지만, OOB 평가는 배깅 학습 중에 자동으로 이루어진다 — 추가 학습 없이 교차 검증 수준의 일반화 오류 추정이 가능하다.
oob_score=True 설정
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
cancer = load_breast_cancer()
X, y = cancer.data, cancer.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
rf = RandomForestClassifier(
n_estimators=200,
oob_score=True, # OOB 점수 계산 활성화
random_state=42,
n_jobs=-1
)
rf.fit(X_train, y_train)
# 세 가지 평가 방법 비교
cv_scores = cross_val_score(
RandomForestClassifier(n_estimators=200, random_state=42, n_jobs=-1),
X, y, cv=5
)
print(f"OOB Score: {rf.oob_score_:.4f}")
print(f"5-Fold CV: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")
print(f"Test Score: {rf.score(X_test, y_test):.4f}")OOB Score: 0.9604
5-Fold CV: 0.9578 ± 0.0238
Test Score: 0.9649
세 점수가 매우 근접하다. OOB가 교차 검증의 훌륭한 대용이 됨을 확인할 수 있다. 특히 데이터가 크거나 학습이 오래 걸릴 때 유용하다.
oob_score=True를 쓰려면 bootstrap=True(기본값)여야 한다. OOB 점수가 테스트 점수와 크게 다르면(예: OOB가 훨씬 낮으면) 데이터에 시간적 순서나 그룹 구조가 있어서 랜덤 분할이 적절하지 않은 신호일 수 있다.
특성 중요도(Feature Importance)
랜덤 포레스트의 강점 중 하나는 어떤 특성이 예측에 중요한지 자동으로 알려준다는 점이다.
계산 방법: Gini 불순도 감소량
각 특성의 중요도는 모든 트리에서 해당 특성으로 분기할 때 줄어드는 Gini 불순도의 평균으로 계산된다.
특성 j의 중요도 = (1/B) Σ_{b=1}^{B} Σ_{노드 t, 분기 특성=j} Δ불순도(t)
여기서 Δ불순도(t) = 부모 노드 불순도 - (왼쪽 자식 가중 불순도 + 오른쪽 자식 가중 불순도)값이 클수록 그 특성이 트리를 만들 때 더 많이 기여했다는 뜻이다. 모든 특성의 중요도 합은 1이다.
특성 중요도 시각화
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
cancer = load_breast_cancer()
X, y = cancer.data, cancer.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
rf = RandomForestClassifier(n_estimators=200, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
# 특성 중요도 추출
importances = rf.feature_importances_
feature_names = cancer.feature_names
sorted_idx = np.argsort(importances)[::-1]
print("상위 10개 중요 특성:")
for i in range(10):
idx = sorted_idx[i]
print(f" {i+1:2d}. {feature_names[idx]:<35s} {importances[idx]:.4f}")상위 10개 중요 특성:
1. worst concave points 0.1521
2. worst perimeter 0.1047
3. worst radius 0.0937
4. mean concave points 0.0835
5. worst area 0.0703
6. mean perimeter 0.0554
7. mean radius 0.0483
8. worst concavity 0.0445
9. mean area 0.0397
10. mean concavity 0.0336
Permutation Importance와 비교
Gini 기반 중요도에는 한계가 있다 — 카디널리티가 높은 특성(연속값, 고유값이 많은 범주형)이 과대평가될 수 있다. 이 문제를 해결하는 것이 Permutation Importance다.
from sklearn.inspection import permutation_importance
perm_result = permutation_importance(
rf, X_test, y_test, n_repeats=10, random_state=42, n_jobs=-1
)
print("Permutation Importance 상위 5개:")
perm_sorted = np.argsort(perm_result.importances_mean)[::-1]
for i in range(5):
idx = perm_sorted[i]
mean = perm_result.importances_mean[idx]
std = perm_result.importances_std[idx]
print(f" {feature_names[idx]:<35s} {mean:.4f} ± {std:.4f}")Permutation Importance 상위 5개:
worst concave points 0.0614 ± 0.0089
worst perimeter 0.0526 ± 0.0072
mean concave points 0.0438 ± 0.0065
worst radius 0.0351 ± 0.0054
worst area 0.0289 ± 0.0048Gini 중요도: 학습 데이터 기준. 빠르게 계산. 상관된 특성이나 고카디널리티 특성에서 편향될 수 있음
Permutation Importance: 검증/테스트 데이터 기준. 더 신뢰할 수 있음. 특성 순서를 무작위로 섞어 성능 감소 측정
중요한 특성 선택 작업에는 Permutation Importance를 권장한다.
하이퍼파라미터 가이드
랜덤 포레스트의 주요 파라미터와 각각의 영향을 정리했다.
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(
n_estimators=100, # 트리 수
max_depth=None, # 최대 깊이 (None = 완전 성장)
max_features='sqrt', # 노드별 고려 특성 수
min_samples_leaf=1, # 리프 노드 최소 샘플 수
min_samples_split=2, # 내부 노드 최소 샘플 수
bootstrap=True, # 부트스트랩 샘플링 여부
oob_score=False, # OOB 점수 계산 여부
n_jobs=-1, # 병렬 처리 (코어 수 -1)
random_state=42
)| 파라미터 | 기본값 | 효과 | 조정 방향 |
|---|---|---|---|
n_estimators |
100 | 많을수록 안정적, 학습 시간 선형 증가 | 100~500 사이에서 OOB로 확인 |
max_depth |
None | 작을수록 과소적합, 클수록 과적합 | None → 5~20 범위로 줄여보기 |
max_features |
‘sqrt’ | 작을수록 트리 다양성↑, 개별 성능↓ | ‘sqrt’(분류), ‘log2’, 0.5 등 시도 |
min_samples_leaf |
1 | 클수록 단순한 트리, 분산 감소 | 과적합이면 5~20으로 증가 |
min_samples_split |
2 | 클수록 덜 세분화 | 과적합이면 10~50으로 증가 |
실전 하이퍼파라미터 탐색
from sklearn.model_selection import RandomizedSearchCV
import numpy as np
param_dist = {
'n_estimators': [100, 200, 300],
'max_depth': [None, 10, 20, 30],
'max_features': ['sqrt', 'log2', 0.5],
'min_samples_leaf': [1, 2, 5, 10],
'min_samples_split': [2, 5, 10],
}
rf_search = RandomizedSearchCV(
RandomForestClassifier(random_state=42, n_jobs=-1),
param_distributions=param_dist,
n_iter=30,
cv=5,
scoring='accuracy',
n_jobs=-1,
random_state=42
)
rf_search.fit(X_train, y_train)
print(f"최적 파라미터: {rf_search.best_params_}")
print(f"CV 최고 점수: {rf_search.best_score_:.4f}")
print(f"테스트 점수: {rf_search.best_estimator_.score(X_test, y_test):.4f}")최적 파라미터: {'n_estimators': 300, 'min_samples_split': 2,
'min_samples_leaf': 1, 'max_features': 'sqrt', 'max_depth': 20}
CV 최고 점수: 0.9648
테스트 점수: 0.9649먼저 기본값
RandomForestClassifier(n_estimators=100, n_jobs=-1)로 베이스라인을 잡자. OOB 점수를 켜고 n_estimators를 늘려가며 수렴점을 찾는다. 그 다음 max_features와 min_samples_leaf를 조정해 분산-편향 균형을 맞춘다.
흔한 실수
1. n_estimators를 너무 작게 잡는다
# ❌ 트리 10개는 너무 불안정
rf_small = RandomForestClassifier(n_estimators=10, random_state=42)
rf_small.fit(X_train, y_train)
print(f"n=10 정확도: {rf_small.score(X_test, y_test):.4f}") # 0.9298 (불안정)
# ✅ 최소 100개부터 시작, OOB로 수렴 확인
rf_good = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf_good.fit(X_train, y_train)
print(f"n=100 정확도: {rf_good.score(X_test, y_test):.4f}") # 0.9649 (안정적)
print(f"OOB 점수: {rf_good.oob_score_:.4f}")n=10 정확도: 0.9298 (불안정)
n=100 정확도: 0.9649 (안정적)
OOB 점수: 0.95602. 특성 스케일링을 걱정한다
# ❌ 랜덤 포레스트에 StandardScaler를 쓸 필요가 없다
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
pipe_wrong = Pipeline([
('scaler', StandardScaler()), # 트리 기반 모델에는 불필요
('rf', RandomForestClassifier(n_estimators=100, random_state=42))
])
pipe_wrong.fit(X_train, y_train)
# ✅ 랜덤 포레스트는 분기점만 찾으므로 스케일에 불변(invariant)
rf_no_scale = RandomForestClassifier(n_estimators=100, random_state=42)
rf_no_scale.fit(X_train, y_train)
print(f"스케일링 O: {pipe_wrong.score(X_test, y_test):.4f}")
print(f"스케일링 X: {rf_no_scale.score(X_test, y_test):.4f}")
# 결과가 동일하다스케일링 O: 0.9649
스케일링 X: 0.9649결정 트리는 분기점 위치만 중요하고, 특성 간 크기 비교를 하지 않으므로 스케일에 완전히 불변이다.
3. Gini 중요도만 믿는다
# ❌ 상관된 특성이 있을 때 Gini 중요도는 오도할 수 있다
# 예: 두 특성이 강하게 상관되면, 중요도가 두 특성에 나뉘어 각각이 낮아 보인다
from sklearn.inspection import permutation_importance
# ✅ 중요한 특성 선택 시 Permutation Importance를 함께 확인한다
perm = permutation_importance(rf, X_test, y_test, n_repeats=30, random_state=42)
# 두 방법의 순위가 크게 다르면 특성 간 상관성을 의심하자
print("Gini Top3:", [cancer.feature_names[i] for i in np.argsort(rf.feature_importances_)[-3:][::-1]])
print("Perm Top3:", [cancer.feature_names[i] for i in np.argsort(perm.importances_mean)[-3:][::-1]])Gini Top3: ['worst concave points', 'worst perimeter', 'worst radius']
Perm Top3: ['worst concave points', 'worst perimeter', 'mean concave points']두 방법의 결과가 크게 다를수록 특성 간 상관성 또는 데이터 구조를 더 살펴봐야 한다.
랜덤 포레스트는 모든 트리를 메모리에 유지한다.
n_estimators=1000, 데이터 수십만 건이면 메모리 이슈가 생길 수 있다. 배포 환경에서 예측 지연이 문제면 n_estimators를 줄이거나, 학습 후 joblib으로 모델을 저장/로드해서 재사용하자. 각 트리 예측이 독립적이므로 n_jobs=-1로 병렬 예측도 가능하다.
마치며
랜덤 포레스트의 핵심은 배깅 + 특성 무작위성이다.
- 특성 무작위성: 각 노드에서
max_features개 특성만 후보로 선택. 트리 간 상관관계 ρ를 줄인다. - 분산 공식: Var(T̄) = ρσ² + (1-ρ)σ²/B. ρ가 줄면 전체 분산이 줄어든다.
- OOB: oob_score=True로 추가 학습 없이 교차 검증 수준 성능 추정.
- 특성 중요도: 어떤 특성이 예측에 기여하는지 자동으로 알려준다.
랜덤 포레스트는 조정이 거의 필요 없는 강력한 베이스라인 모델이다. 기본값으로도 대부분의 데이터셋에서 좋은 성능을 낸다. 실전에서 가장 먼저 시도할 모델 중 하나다.
다음 글에서는 앙상블의 또 다른 축인 부스팅(Boosting) 을 다룬다. 배깅이 트리를 병렬로 독립적으로 쌓는다면, 부스팅은 트리를 순차적으로 쌓으면서 이전 트리가 틀린 샘플에 더 집중한다 — 편향을 줄이는 방향으로. AdaBoost와 Gradient Boosting의 원리를 다음 글에서 파헤쳐보자.
- 랜덤 포레스트: 배깅 + 노드별 특성 서브샘플링(
max_features). 트리 간 상관관계 ρ 감소 →Var(T̄) = ρσ² + (1-ρ)σ²/B전체 감소 max_features: 분류는 √p, 회귀는 p/3이 좋은 출발점. 작을수록 다양성↑, 개별 성능↓- OOB Score: oob_score=True로 추가 학습 없이 교차 검증 수준 성능 추정
- 특성 중요도: Gini 감소량 기반 (빠름, 편향 가능) vs Permutation Importance (신뢰성 높음)
- 스케일 불변: 트리 기반 모델은 특성 스케일링 불필요
- 하이퍼파라미터:
n_estimators=100~300,max_features='sqrt', OOB로 수렴 확인
참고자료
- Leo Breiman — “Random Forests” (2001), Machine Learning 45:5–32
- Leo Breiman — “Bagging Predictors” (1996), Machine Learning 24:123–140
- Scikit-learn — RandomForestClassifier Documentation
- Scikit-learn — Ensemble Methods User Guide
- StatQuest with Josh Starmer — Random Forests (YouTube)
- Trevor Hastie et al. — “The Elements of Statistical Learning”, Chapter 15