콘텐츠로 이동

Random Forest (랜덤포레스트)

논문 정보

항목 내용
제목 Random Forests
저자 Leo Breiman
학회/저널 Machine Learning
연도 2001
링크 https://link.springer.com/article/10.1023/A:1010933404324

개요

문제 정의

단일 의사결정나무의 한계를 극복:

  • 높은 분산 (variance): 데이터가 조금만 바뀌어도 트리가 크게 달라짐
  • 과적합 경향: 깊은 트리는 훈련 데이터에 과적합
  • 일반화 성능 한계: 단일 모델의 한계

핵심 아이디어

Bagging + 특성 무작위 선택:

  1. Bootstrap Aggregating (Bagging): 부트스트랩 샘플로 다수의 트리 학습
  2. Random Subspace: 각 분할에서 특성의 부분집합만 고려
  3. 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%)로 검증:

P(샘플 i가 트리 b에 포함되지 않음) = (1 - 1/n)^n ≈ 1/e ≈ 0.368

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