앙상블 학습 - 배깅 (Bagging)
앙상블 학습 - 배깅 (Bagging)¶
개요¶
배깅(Bagging, Bootstrap Aggregating)은 여러 개의 기본 모델을 학습시켜 그 결과를 종합하는 앙상블 기법입니다. 대표적인 알고리즘으로 Random Forest가 있습니다.
1. 앙상블 학습의 기본 개념¶
1.1 앙상블이란?¶
"""
앙상블 학습 (Ensemble Learning):
- 여러 개의 약한 학습기(weak learner)를 결합하여 강한 학습기 생성
- "군중의 지혜" (Wisdom of Crowds)
앙상블의 주요 유형:
1. 배깅 (Bagging): 병렬 학습, 분산 감소
- Random Forest
- Bagging Classifier/Regressor
2. 부스팅 (Boosting): 순차 학습, 편향 감소
- AdaBoost
- Gradient Boosting
- XGBoost, LightGBM
3. 스태킹 (Stacking): 메타 모델 학습
- 다양한 모델의 예측을 입력으로 사용
4. 보팅 (Voting): 단순 투표
- Hard Voting, Soft Voting
"""
1.2 배깅의 원리¶
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
# 부트스트랩 샘플링 시각화
np.random.seed(42)
original_data = np.arange(10)
print("부트스트랩 샘플링 예시:")
print(f"원본 데이터: {original_data}")
for i in range(3):
bootstrap_sample = np.random.choice(original_data, size=len(original_data), replace=True)
oob = set(original_data) - set(bootstrap_sample)
print(f"샘플 {i+1}: {bootstrap_sample} (OOB: {oob})")
# 부트스트랩 샘플에서 OOB 비율
"""
기대되는 OOB 비율:
- 각 샘플이 선택되지 않을 확률 = (1 - 1/n)^n
- n이 커지면 → e^(-1) ≈ 0.368 (약 37%)
- 즉, 각 모델은 원본 데이터의 약 63%만 사용
"""
n = 1000
selected = np.zeros(n)
for _ in range(n):
idx = np.random.randint(0, n)
selected[idx] = 1
oob_ratio = 1 - np.mean(selected)
print(f"\n실험적 OOB 비율: {oob_ratio:.4f}")
print(f"이론적 OOB 비율: {1/np.e:.4f}")
2. 직접 구현하는 배깅¶
from sklearn.base import clone
class SimpleBagging:
"""간단한 배깅 구현"""
def __init__(self, base_estimator, n_estimators=10, random_state=None):
self.base_estimator = base_estimator
self.n_estimators = n_estimators
self.random_state = random_state
self.estimators_ = []
self.oob_indices_ = []
def fit(self, X, y):
np.random.seed(self.random_state)
n_samples = len(X)
self.estimators_ = []
self.oob_indices_ = []
for _ in range(self.n_estimators):
# 부트스트랩 샘플링
indices = np.random.choice(n_samples, size=n_samples, replace=True)
oob_indices = list(set(range(n_samples)) - set(indices))
X_bootstrap = X[indices]
y_bootstrap = y[indices]
# 모델 학습
estimator = clone(self.base_estimator)
estimator.fit(X_bootstrap, y_bootstrap)
self.estimators_.append(estimator)
self.oob_indices_.append(oob_indices)
return self
def predict(self, X):
# 각 모델의 예측 수집
predictions = np.array([est.predict(X) for est in self.estimators_])
# 다수결 투표
return np.apply_along_axis(
lambda x: np.bincount(x.astype(int)).argmax(),
axis=0,
arr=predictions
)
def predict_proba(self, X):
# 확률 평균
probas = np.array([est.predict_proba(X) for est in self.estimators_])
return np.mean(probas, axis=0)
# 테스트
X, y = make_classification(n_samples=500, n_features=20, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 단일 트리 vs 배깅
single_tree = DecisionTreeClassifier(random_state=42)
single_tree.fit(X_train, y_train)
bagging = SimpleBagging(DecisionTreeClassifier(), n_estimators=10, random_state=42)
bagging.fit(X_train, y_train)
print("배깅 효과 비교:")
print(f" 단일 결정 트리: {single_tree.score(X_test, y_test):.4f}")
print(f" 배깅 (10 trees): {np.mean(bagging.predict(X_test) == y_test):.4f}")
3. sklearn의 BaggingClassifier¶
from sklearn.ensemble import BaggingClassifier, BaggingRegressor
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
# BaggingClassifier 사용
bagging_clf = BaggingClassifier(
estimator=DecisionTreeClassifier(),
n_estimators=100,
max_samples=1.0, # 각 부트스트랩 샘플 크기 (비율)
max_features=1.0, # 각 모델에서 사용할 특성 비율
bootstrap=True, # 부트스트랩 샘플링 사용
bootstrap_features=False, # 특성 부트스트랩
oob_score=True, # OOB 점수 계산
n_jobs=-1, # 병렬 처리
random_state=42
)
bagging_clf.fit(X_train, y_train)
y_pred = bagging_clf.predict(X_test)
print("BaggingClassifier 결과:")
print(f" 훈련 정확도: {bagging_clf.score(X_train, y_train):.4f}")
print(f" 테스트 정확도: {accuracy_score(y_test, y_pred):.4f}")
print(f" OOB 점수: {bagging_clf.oob_score_:.4f}")
3.1 모델 수에 따른 성능 변화¶
# 모델 수 증가에 따른 성능 변화
n_estimators_range = [1, 5, 10, 20, 50, 100, 200]
train_scores = []
test_scores = []
oob_scores = []
for n_est in n_estimators_range:
clf = BaggingClassifier(
estimator=DecisionTreeClassifier(),
n_estimators=n_est,
oob_score=True,
random_state=42,
n_jobs=-1
)
clf.fit(X_train, y_train)
train_scores.append(clf.score(X_train, y_train))
test_scores.append(clf.score(X_test, y_test))
oob_scores.append(clf.oob_score_)
# 시각화
plt.figure(figsize=(10, 6))
plt.plot(n_estimators_range, train_scores, 'o-', label='Train')
plt.plot(n_estimators_range, test_scores, 's-', label='Test')
plt.plot(n_estimators_range, oob_scores, '^-', label='OOB')
plt.xlabel('Number of Estimators')
plt.ylabel('Accuracy')
plt.title('Bagging: Performance vs Number of Estimators')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
4. Random Forest¶
4.1 기본 사용법¶
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.datasets import load_iris
# 데이터 로드
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.2, random_state=42
)
# Random Forest 분류기
rf_clf = RandomForestClassifier(
n_estimators=100, # 트리 수
max_depth=None, # 최대 깊이
min_samples_split=2, # 분할 최소 샘플
min_samples_leaf=1, # 리프 최소 샘플
max_features='sqrt', # 분할 시 고려할 특성 수
bootstrap=True, # 부트스트랩 샘플링
oob_score=True, # OOB 점수
n_jobs=-1, # 병렬 처리
random_state=42
)
rf_clf.fit(X_train, y_train)
print("Random Forest 결과:")
print(f" 훈련 정확도: {rf_clf.score(X_train, y_train):.4f}")
print(f" 테스트 정확도: {rf_clf.score(X_test, y_test):.4f}")
print(f" OOB 점수: {rf_clf.oob_score_:.4f}")
4.2 Random Forest vs 일반 Bagging¶
"""
Random Forest와 Bagging의 차이:
1. 특성 무작위 선택:
- Bagging: 모든 특성 사용 (max_features=1.0)
- Random Forest: sqrt(n_features) 또는 log2(n_features) 사용
2. 트리 상관관계:
- Bagging: 트리 간 상관관계 높음
- Random Forest: 트리 간 상관관계 낮음 (다양성 증가)
3. 분산 감소:
- Var(average) = Var(single) / n + (n-1)/n * Cov
- 상관관계(Cov)가 낮을수록 분산 더 감소
"""
# 비교 실험
bagging = BaggingClassifier(
estimator=DecisionTreeClassifier(),
n_estimators=100,
max_features=1.0, # 모든 특성 사용
random_state=42,
n_jobs=-1
)
rf = RandomForestClassifier(
n_estimators=100,
max_features='sqrt', # sqrt(n_features) 사용
random_state=42,
n_jobs=-1
)
bagging.fit(X_train, y_train)
rf.fit(X_train, y_train)
print("Bagging vs Random Forest:")
print(f" Bagging 정확도: {bagging.score(X_test, y_test):.4f}")
print(f" Random Forest 정확도: {rf.score(X_test, y_test):.4f}")
4.3 max_features 파라미터¶
# max_features에 따른 성능 변화
from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, test_size=0.2, random_state=42
)
n_features = X_train.shape[1]
max_features_options = [1, 'sqrt', 'log2', 0.5, n_features]
print("max_features에 따른 성능:")
for max_feat in max_features_options:
rf = RandomForestClassifier(
n_estimators=100,
max_features=max_feat,
random_state=42,
n_jobs=-1
)
rf.fit(X_train, y_train)
print(f" max_features={max_feat}: {rf.score(X_test, y_test):.4f}")
5. 특성 중요도 (Feature Importance)¶
5.1 기본 특성 중요도¶
# Random Forest 학습
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
# 특성 중요도
importances = rf.feature_importances_
indices = np.argsort(importances)[::-1]
# 시각화
plt.figure(figsize=(12, 6))
plt.bar(range(len(importances)), importances[indices])
plt.xticks(range(len(importances)),
[cancer.feature_names[i] for i in indices],
rotation=90)
plt.ylabel('Feature Importance')
plt.title('Random Forest Feature Importance')
plt.tight_layout()
plt.show()
# 상위 10개 특성
print("\n상위 10개 특성:")
for i in range(10):
print(f" {i+1}. {cancer.feature_names[indices[i]]}: {importances[indices[i]]:.4f}")
5.2 특성 중요도 해석 방법¶
"""
특성 중요도 계산 방법:
1. 불순도 기반 중요도 (Mean Decrease in Impurity, MDI):
- 각 특성이 분할에 사용될 때 불순도 감소량의 평균
- feature_importances_ 기본값
- 단점: 고카디널리티 특성에 편향
2. 순열 중요도 (Permutation Importance):
- 특성 값을 무작위로 섞었을 때 성능 감소 측정
- 더 신뢰성 있는 중요도
"""
from sklearn.inspection import permutation_importance
# 순열 중요도 계산
perm_importance = permutation_importance(
rf, X_test, y_test,
n_repeats=30,
random_state=42,
n_jobs=-1
)
# 비교 시각화
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
# MDI (불순도 기반)
sorted_idx_mdi = rf.feature_importances_.argsort()[-10:]
axes[0].barh(range(10), rf.feature_importances_[sorted_idx_mdi])
axes[0].set_yticks(range(10))
axes[0].set_yticklabels([cancer.feature_names[i] for i in sorted_idx_mdi])
axes[0].set_title('MDI (Impurity-based) Feature Importance')
# 순열 중요도
sorted_idx_perm = perm_importance.importances_mean.argsort()[-10:]
axes[1].barh(range(10), perm_importance.importances_mean[sorted_idx_perm])
axes[1].set_yticks(range(10))
axes[1].set_yticklabels([cancer.feature_names[i] for i in sorted_idx_perm])
axes[1].set_title('Permutation Feature Importance')
plt.tight_layout()
plt.show()
5.3 특성 선택에 활용¶
from sklearn.feature_selection import SelectFromModel
# 중요도 기반 특성 선택
selector = SelectFromModel(
RandomForestClassifier(n_estimators=100, random_state=42),
threshold='median' # 중요도 중간값 이상인 특성만 선택
)
selector.fit(X_train, y_train)
# 선택된 특성
selected_features = cancer.feature_names[selector.get_support()]
print(f"선택된 특성 수: {len(selected_features)}")
print(f"선택된 특성: {list(selected_features)}")
# 선택된 특성으로 학습
X_train_selected = selector.transform(X_train)
X_test_selected = selector.transform(X_test)
rf_selected = RandomForestClassifier(n_estimators=100, random_state=42)
rf_selected.fit(X_train_selected, y_train)
print(f"\n전체 특성 정확도: {rf.score(X_test, y_test):.4f}")
print(f"선택된 특성 정확도: {rf_selected.score(X_test_selected, y_test):.4f}")
6. OOB (Out-of-Bag) 에러¶
6.1 OOB 점수 이해¶
"""
OOB (Out-of-Bag) 에러:
- 각 트리는 부트스트랩 샘플로 학습
- 각 샘플은 평균 37%의 트리에서 OOB (학습에 사용되지 않음)
- OOB 샘플로 검증 → 별도 검증 세트 불필요
장점:
1. 추가 데이터 분할 불필요
2. 교차검증과 유사한 효과
3. 학습과 동시에 검증 가능
"""
# OOB 점수 활용
rf = RandomForestClassifier(
n_estimators=100,
oob_score=True,
random_state=42,
n_jobs=-1
)
rf.fit(X_train, y_train)
print("OOB 점수 분석:")
print(f" OOB 점수: {rf.oob_score_:.4f}")
print(f" 테스트 점수: {rf.score(X_test, y_test):.4f}")
# OOB 예측 확률
print(f"\nOOB 예측 확률 (처음 5개 샘플):")
print(rf.oob_decision_function_[:5])
6.2 OOB vs 교차검증 비교¶
from sklearn.model_selection import cross_val_score
# 교차검증
cv_scores = cross_val_score(
RandomForestClassifier(n_estimators=100, random_state=42),
X_train, y_train, cv=5
)
# OOB
rf_oob = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf_oob.fit(X_train, y_train)
print("OOB vs 교차검증 비교:")
print(f" OOB 점수: {rf_oob.oob_score_:.4f}")
print(f" CV 평균 점수: {cv_scores.mean():.4f} (+/- {cv_scores.std():.4f})")
7. 하이퍼파라미터 튜닝¶
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from scipy.stats import randint, uniform
# Grid Search
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [None, 10, 20, 30],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4],
'max_features': ['sqrt', 'log2', 0.5]
}
# 더 효율적인 Randomized Search
param_distributions = {
'n_estimators': randint(50, 300),
'max_depth': [None] + list(range(5, 31)),
'min_samples_split': randint(2, 21),
'min_samples_leaf': randint(1, 11),
'max_features': uniform(0.1, 0.9)
}
random_search = RandomizedSearchCV(
RandomForestClassifier(random_state=42),
param_distributions,
n_iter=50,
cv=5,
scoring='accuracy',
random_state=42,
n_jobs=-1
)
random_search.fit(X_train, y_train)
print("하이퍼파라미터 튜닝 결과:")
print(f" 최적 파라미터: {random_search.best_params_}")
print(f" 최적 CV 점수: {random_search.best_score_:.4f}")
print(f" 테스트 점수: {random_search.score(X_test, y_test):.4f}")
8. Random Forest 회귀¶
from sklearn.datasets import load_diabetes
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score
# 데이터 로드
diabetes = load_diabetes()
X_train, X_test, y_train, y_test = train_test_split(
diabetes.data, diabetes.target, test_size=0.2, random_state=42
)
# Random Forest 회귀
rf_reg = RandomForestRegressor(
n_estimators=100,
max_depth=None,
min_samples_split=2,
random_state=42,
n_jobs=-1
)
rf_reg.fit(X_train, y_train)
y_pred = rf_reg.predict(X_test)
print("Random Forest 회귀 결과:")
print(f" MSE: {mean_squared_error(y_test, y_pred):.4f}")
print(f" RMSE: {np.sqrt(mean_squared_error(y_test, y_pred)):.4f}")
print(f" R²: {r2_score(y_test, y_pred):.4f}")
# 실제값 vs 예측값
plt.figure(figsize=(8, 6))
plt.scatter(y_test, y_pred, alpha=0.7)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', linewidth=2)
plt.xlabel('Actual')
plt.ylabel('Predicted')
plt.title(f'Random Forest Regression (R² = {r2_score(y_test, y_pred):.4f})')
plt.grid(True, alpha=0.3)
plt.show()
9. Extra Trees (Extremely Randomized Trees)¶
from sklearn.ensemble import ExtraTreesClassifier, ExtraTreesRegressor
"""
Extra Trees vs Random Forest:
1. 분할점 선택:
- Random Forest: 각 특성의 최적 분할점 선택
- Extra Trees: 각 특성에서 무작위 분할점 선택
2. 부트스트랩:
- Random Forest: 기본적으로 부트스트랩 사용
- Extra Trees: 기본적으로 전체 데이터 사용
3. 특성:
- Extra Trees: 더 빠름, 더 많은 무작위성
- Random Forest: 일반적으로 더 좋은 성능
"""
# 비교
rf = RandomForestClassifier(n_estimators=100, random_state=42)
et = ExtraTreesClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
et.fit(X_train, y_train)
print("Random Forest vs Extra Trees:")
print(f" Random Forest: {rf.score(X_test, y_test):.4f}")
print(f" Extra Trees: {et.score(X_test, y_test):.4f}")
10. Voting Classifier¶
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier
# 다양한 모델 정의
clf1 = LogisticRegression(random_state=42, max_iter=1000)
clf2 = RandomForestClassifier(n_estimators=50, random_state=42)
clf3 = SVC(probability=True, random_state=42)
# Hard Voting (다수결)
hard_voting = VotingClassifier(
estimators=[
('lr', clf1),
('rf', clf2),
('svc', clf3)
],
voting='hard'
)
# Soft Voting (확률 평균)
soft_voting = VotingClassifier(
estimators=[
('lr', clf1),
('rf', clf2),
('svc', clf3)
],
voting='soft'
)
# 학습 및 비교
print("Voting Classifier 비교:")
for clf, label in [(clf1, 'Logistic'), (clf2, 'RF'), (clf3, 'SVC'),
(hard_voting, 'Hard Voting'), (soft_voting, 'Soft Voting')]:
clf.fit(X_train, y_train)
score = clf.score(X_test, y_test)
print(f" {label}: {score:.4f}")
연습 문제¶
문제 1: Random Forest 분류¶
유방암 데이터로 Random Forest를 학습하고 특성 중요도를 분석하세요.
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
cancer = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
cancer.data, cancer.target, test_size=0.2, random_state=42
)
# 풀이
rf = RandomForestClassifier(n_estimators=100, oob_score=True, random_state=42)
rf.fit(X_train, y_train)
print(f"테스트 정확도: {rf.score(X_test, y_test):.4f}")
print(f"OOB 점수: {rf.oob_score_:.4f}")
print("\n상위 5개 특성:")
indices = np.argsort(rf.feature_importances_)[::-1][:5]
for i, idx in enumerate(indices):
print(f" {i+1}. {cancer.feature_names[idx]}: {rf.feature_importances_[idx]:.4f}")
문제 2: 하이퍼파라미터 튜닝¶
Grid Search로 최적의 Random Forest 파라미터를 찾으세요.
from sklearn.model_selection import GridSearchCV
param_grid = {
'n_estimators': [50, 100],
'max_depth': [5, 10, None],
'min_samples_leaf': [1, 2, 5]
}
# 풀이
grid_search = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=5,
scoring='accuracy',
n_jobs=-1
)
grid_search.fit(X_train, y_train)
print(f"최적 파라미터: {grid_search.best_params_}")
print(f"최적 CV 점수: {grid_search.best_score_:.4f}")
print(f"테스트 점수: {grid_search.score(X_test, y_test):.4f}")
문제 3: Voting Ensemble¶
여러 모델을 결합한 Voting Classifier를 만드세요.
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
# 풀이
voting_clf = VotingClassifier(
estimators=[
('lr', LogisticRegression(max_iter=1000)),
('rf', RandomForestClassifier(n_estimators=50)),
('dt', DecisionTreeClassifier(max_depth=5))
],
voting='soft'
)
voting_clf.fit(X_train, y_train)
print(f"Voting 정확도: {voting_clf.score(X_test, y_test):.4f}")
요약¶
| 모델 | 특징 | 장점 | 단점 |
|---|---|---|---|
| Bagging | 부트스트랩 + 평균 | 분산 감소, 과적합 방지 | 해석 어려움 |
| Random Forest | 배깅 + 특성 랜덤 | 높은 성능, 특성 중요도 | 많은 계산량 |
| Extra Trees | 완전 랜덤 분할 | 빠른 학습 | RF보다 낮은 성능 가능 |
| Voting | 다양한 모델 결합 | 다양성 활용 | 개별 모델 튜닝 필요 |
Random Forest 하이퍼파라미터 가이드¶
| 파라미터 | 기본값 | 권장 범위 | 효과 |
|---|---|---|---|
| n_estimators | 100 | 100-500 | 많을수록 안정적 |
| max_depth | None | 10-30 | 과적합 제어 |
| min_samples_split | 2 | 2-20 | 과적합 제어 |
| min_samples_leaf | 1 | 1-10 | 과적합 제어 |
| max_features | 'sqrt' | 'sqrt', 'log2', 0.3-0.7 | 트리 다양성 |