Random Forest (랜덤포레스트)¶
논문 정보¶
| 항목 | 내용 |
|---|---|
| 제목 | Random Forests |
| 저자 | Leo Breiman |
| 학회/저널 | Machine Learning |
| 연도 | 2001 |
| 링크 | https://link.springer.com/article/10.1023/A:1010933404324 |
개요¶
문제 정의¶
단일 의사결정나무의 한계를 극복:
- 높은 분산 (variance): 데이터가 조금만 바뀌어도 트리가 크게 달라짐
- 과적합 경향: 깊은 트리는 훈련 데이터에 과적합
- 일반화 성능 한계: 단일 모델의 한계
핵심 아이디어¶
Bagging + 특성 무작위 선택:
- Bootstrap Aggregating (Bagging): 부트스트랩 샘플로 다수의 트리 학습
- Random Subspace: 각 분할에서 특성의 부분집합만 고려
- Averaging/Voting: 예측 시 모든 트리의 결과를 집계
이 조합으로 트리 간 상관관계를 낮추고, 분산을 효과적으로 감소.
알고리즘/수식¶
앙상블 구성¶
Algorithm: Random Forest
Input: 학습 데이터 D = {(x_i, y_i)}, 트리 수 B, 특성 수 m
Output: 앙상블 모델 f
for b = 1 to B:
# Bootstrap sampling
D_b = Bootstrap(D, n) # n개 복원 추출
# Train tree with random feature selection
T_b = GrowTree(D_b, m)
# Prediction
for classification:
f(x) = majority_vote({T_1(x), ..., T_B(x)})
for regression:
f(x) = (1/B) * sum_{b=1}^{B} T_b(x)
개별 트리 학습¶
function GrowTree(D, m):
if stopping_condition(D):
return LeafNode(prediction)
# Random feature selection (key difference from Bagging)
F_subset = random_sample(all_features, size=m)
# Find best split using only F_subset
best_split = find_best_split(D, F_subset)
D_left, D_right = split(D, best_split)
node.left = GrowTree(D_left, m)
node.right = GrowTree(D_right, m)
return node
특성 수 권장값¶
| 문제 유형 | 권장 m | 설명 |
|---|---|---|
| 분류 | sqrt(d) | d = 전체 특성 수 |
| 회귀 | d/3 | 더 많은 특성 사용 |
Out-of-Bag (OOB) Error¶
각 트리에서 사용하지 않은 샘플(~37%)로 검증:
OOB 에러는 교차 검증 없이 일반화 오차 추정 가능.
시간 복잡도¶
| 단계 | 복잡도 |
|---|---|
| 학습 | O(B * n * m * log(n)) |
| 예측 | O(B * log(n)) |
하이퍼파라미터 가이드¶
| 파라미터 | 설명 | 권장 범위 | 기본값 |
|---|---|---|---|
| n_estimators | 트리 수 | 100 ~ 1000 | 100 |
| max_depth | 최대 트리 깊이 | None 또는 10 ~ 30 | None |
| max_features | 분할 시 특성 수 | 'sqrt', 'log2', float | 'sqrt' |
| min_samples_split | 분할 최소 샘플 | 2 ~ 10 | 2 |
| min_samples_leaf | 리프 최소 샘플 | 1 ~ 5 | 1 |
| bootstrap | 부트스트랩 사용 | True/False | True |
| oob_score | OOB 점수 계산 | True/False | False |
| n_jobs | 병렬 처리 | -1 (전체 코어) | None |
| class_weight | 클래스 가중치 | 'balanced', 'balanced_subsample' | None |
튜닝 우선순위: 1. n_estimators: 많을수록 좋음 (수렴 후 증가 효과 없음) 2. max_depth: 과적합 방지 (None이면 완전 성장) 3. max_features: 트리 간 다양성 조절 4. min_samples_leaf: 과적합 방지
Python 코드 예시¶
기본 사용법¶
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.datasets import load_breast_cancer, fetch_california_housing
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import accuracy_score, classification_report, r2_score
import matplotlib.pyplot as plt
# 분류 데이터 로드
data = load_breast_cancer()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Random Forest 분류
rf_clf = RandomForestClassifier(
n_estimators=100,
max_depth=None,
max_features='sqrt',
min_samples_leaf=1,
bootstrap=True,
oob_score=True,
n_jobs=-1,
random_state=42
)
rf_clf.fit(X_train, y_train)
# 평가
y_pred = rf_clf.predict(X_test)
print("=== Random Forest Classification ===")
print(f"Test Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print(f"OOB Score: {rf_clf.oob_score_:.4f}")
print(f"\nClassification Report:")
print(classification_report(y_test, y_pred, target_names=data.target_names))
특성 중요도¶
# Gini importance (Mean Decrease in Impurity)
feature_importance = pd.DataFrame({
'feature': data.feature_names,
'importance': rf_clf.feature_importances_
}).sort_values('importance', ascending=False)
print("\nTop 10 Important Features:")
print(feature_importance.head(10).to_string(index=False))
# 시각화
fig, ax = plt.subplots(figsize=(10, 8))
top_features = feature_importance.head(15)
ax.barh(top_features['feature'], top_features['importance'])
ax.set_xlabel('Importance')
ax.set_title('Random Forest Feature Importance')
ax.invert_yaxis()
plt.tight_layout()
plt.savefig('rf_importance.png', dpi=150)
plt.show()
Permutation Importance (더 신뢰성 있는 중요도)¶
from sklearn.inspection import permutation_importance
# Permutation importance 계산
perm_importance = permutation_importance(
rf_clf, X_test, y_test,
n_repeats=10,
random_state=42,
n_jobs=-1
)
perm_feature_importance = pd.DataFrame({
'feature': data.feature_names,
'importance_mean': perm_importance.importances_mean,
'importance_std': perm_importance.importances_std
}).sort_values('importance_mean', ascending=False)
print("\nPermutation Importance (Top 10):")
print(perm_feature_importance.head(10).to_string(index=False))
트리 수에 따른 성능 변화¶
# 트리 수에 따른 OOB error 추적
n_estimators_range = range(10, 501, 10)
oob_errors = []
for n in n_estimators_range:
rf = RandomForestClassifier(
n_estimators=n,
oob_score=True,
n_jobs=-1,
random_state=42
)
rf.fit(X_train, y_train)
oob_errors.append(1 - rf.oob_score_)
plt.figure(figsize=(10, 6))
plt.plot(n_estimators_range, oob_errors)
plt.xlabel('Number of Trees')
plt.ylabel('OOB Error')
plt.title('OOB Error vs Number of Trees')
plt.savefig('rf_n_estimators.png', dpi=150)
plt.show()
print(f"Optimal n_estimators (lowest OOB error): {n_estimators_range[np.argmin(oob_errors)]}")
하이퍼파라미터 튜닝¶
from sklearn.model_selection import RandomizedSearchCV
param_distributions = {
'n_estimators': [100, 200, 300, 500],
'max_depth': [None, 10, 20, 30],
'max_features': ['sqrt', 'log2', 0.3, 0.5],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4]
}
random_search = RandomizedSearchCV(
RandomForestClassifier(random_state=42, n_jobs=-1),
param_distributions,
n_iter=50,
cv=5,
scoring='accuracy',
n_jobs=-1,
random_state=42
)
random_search.fit(X_train, y_train)
print(f"\nBest Parameters: {random_search.best_params_}")
print(f"Best CV Score: {random_search.best_score_:.4f}")
회귀 문제¶
# 회귀 데이터 로드
housing = fetch_california_housing()
X_reg, y_reg = housing.data[:5000], housing.target[:5000]
X_train_reg, X_test_reg, y_train_reg, y_test_reg = train_test_split(
X_reg, y_reg, test_size=0.2, random_state=42
)
# Random Forest 회귀
rf_reg = RandomForestRegressor(
n_estimators=100,
max_depth=None,
max_features=0.33, # 회귀는 d/3 권장
oob_score=True,
n_jobs=-1,
random_state=42
)
rf_reg.fit(X_train_reg, y_train_reg)
y_pred_reg = rf_reg.predict(X_test_reg)
print("\n=== Random Forest Regression ===")
print(f"R2 Score: {r2_score(y_test_reg, y_pred_reg):.4f}")
print(f"OOB Score: {rf_reg.oob_score_:.4f}")
개별 트리 접근¶
# 첫 번째 트리 확인
first_tree = rf_clf.estimators_[0]
print(f"Number of trees: {len(rf_clf.estimators_)}")
print(f"First tree depth: {first_tree.get_depth()}")
print(f"First tree leaves: {first_tree.get_n_leaves()}")
# 개별 트리 예측 확인 (앙상블 다양성)
sample_idx = 0
individual_predictions = [tree.predict([X_test[sample_idx]])[0]
for tree in rf_clf.estimators_]
print(f"\nSample {sample_idx} - Individual tree predictions:")
print(f"Class 0 votes: {individual_predictions.count(0)}")
print(f"Class 1 votes: {individual_predictions.count(1)}")
print(f"Final prediction: {rf_clf.predict([X_test[sample_idx]])[0]}")
언제 쓰나?¶
적합한 상황: - 범용 분류/회귀 문제 (베이스라인으로 최적) - 특성 중요도 분석이 필요할 때 - 이상치에 강건한 모델이 필요할 때 - 별도 튜닝 없이 좋은 성능이 필요할 때 - 병렬 처리 가능한 환경
부적합한 상황: - 실시간 예측 (트리 수만큼 연산) - 메모리 제약 환경 (모든 트리 저장) - 외삽이 중요한 경우 - 극도로 희소한 고차원 데이터 (텍스트 등)
장단점¶
| 장점 | 단점 |
|---|---|
| 높은 정확도 | 단일 트리보다 해석 어려움 |
| 과적합에 강건 | 메모리 사용량 높음 |
| 특성 스케일링 불필요 | 예측 속도 느림 (vs 단일 모델) |
| OOB로 검증 가능 | 외삽 불가능 |
| 결측치/이상치에 강건 | 매우 큰 데이터에서 느림 |
| 병렬화 용이 | 특성 간 선형 관계 비효율적 학습 |
| 튜닝 없이도 좋은 성능 | Gradient Boosting보다 성능 낮음 (일반적) |
관련 논문/참고¶
- Breiman, L. (2001). "Random forests". Machine Learning.
- Breiman, L. (1996). "Bagging predictors". Machine Learning.
- Louppe, G. (2014). "Understanding Random Forests". PhD thesis.
- scikit-learn documentation: https://scikit-learn.org/stable/modules/ensemble.html#forest