Ensemble (앙상블), Voting (투표), Stacking (스태킹), Bagging (배깅), Pasting (페이스팅), Random Forest (랜덤 포레스트)
머신러닝 프로젝트를 진행하고 모델 간의 성능을 비교할 때, 앙상블 모델이 대부분 상위에 랭크되는 것을 자주 확인할 수 있다. 그렇다면 왜 앙상블 모델이 보편적으로 뛰어난 성능을 보일까? 그리고 어떤 종류의 앙상블 모델들이 있는지 알아보자.
Ensemble
앙상블은 여러 모델을 결합하여 개별 모델의 약점을 보완하고 전체적인 예측 성능을 향상시킨다.
하지만, 여러 모델을 결합한다고 무조건 개별 모델보다 좋은 성능을 기록하는 것은 아니다.
앙상블 모델이 개별 모델보다 성능이 향상되기 위해 다음 두 가지 특성이 포함되어야 한다.
다양성과 정확성
각 개별 모델 간의 충분한 다양성과 개별 모델의 정확성이 충족되어야 앙상블 모델의 성능이 개별 모델보다 향상될 수 있다.

- (a) : 개별 모델간 다양성과 개별 모델의 정확성이 모두 충족 -> 성능 향상
- (b) : 다양성 충족 X -> 성능 동일
- (c) : 정확성 충족 X -> 성능 하락
(b)에서 3개의 개별 모델은 모든 샘플에서 같은 결과를 나타낸다. 즉, 모델 간의 다양성이 충족되지 않아 앙상블의 효과를 거두지 못한 예시이다.
(c)에서는 개별 모델간 다양성은 충족되었지만, 3개의 모델이 모두 1/3의 낮은 정확성을 보이며 오히려 앙상블 모델에서 부작용이 발생한 예시이다.
Voting
위 예시에서는 각 개별 모델들에 대해 다수결 투표 (Voting)를 통해 앙상블 모델이 최종 예측을 수행한 것을 확인할 수 있다.
이렇게 개별 모델을 결합하여 최종 결정을 내리는 과정에서 여러 결합 전략을 사용할 수 있다.
- Rule-based learning
- 분류 문제 : Voting (투표법)
- Hard Voting, Soft Voting
- 회귀 문제 : Averaging (평균법)
- Meta-learning
- Stacking (스태킹)
먼저, Voting은 각 개별 모델의 예측을 집계하는 방법으로, Hard Voting (직접 투표)은 가장 많은 표를 얻은 클래스가 앙상블의 최종 예측이 되는 방법이다.

위의 그림처럼 가장 많은 표를 얻은 1번 클래스가 앙상블의 최종 예측 클래스가 된다.
한편, 각 개별 모델이 클래스의 확률을 예측할 수 있다면, 각 모델의 예측을 평균 내어 확률이 가장 높은 클래스를 예측할 수 있으며, 이는 Soft Voting (간접 투표) 방법이다.
사이킷런의 VotingClassifier를 통해 투표 기반 분류기를 생성하고 훈련할 수 있다. 아래 예시에서는 로지스틱 회귀, 랜덤 포레스트, SVC 3가지의 개별 모델의 예측을 집계하여 최종 예측을 결정하며, voting 매개변수를 “soft”로 바꾸면 모든 개별 모델이 클래스의 확률을 추정할 수 있다.
# make_moons 함수를 사용하여 데이터셋 생성
X, y = make_moons(n_samples=500, noise=0.30, random_state=42)
# 생성한 데이터를 학습용 데이터와 테스트용 데이터로 분할
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
# 세 가지 분류 모델을 결합한 VotingClassifier 생성
voting_clf = VotingClassifier(
estimators=[
('lr', LogisticRegression(random_state=42)), # 로지스틱 회귀 모델
('rf', RandomForestClassifier(random_state=42)), # 랜덤 포레스트 모델
('svc', SVC(random_state=42)) # 서포트 벡터 머신 모델
])
# VotingClassifier를 학습용 데이터로 학습시킴
voting_clf.fit(X_train, y_train)
# 각 개별 분류 모델의 테스트셋 성능 출력
for name, clf in voting_clf.named_estimators_.items():
print(name, "=", clf.score(X_test, y_test))
# 투표 기반 분류기의 테스트셋 성능 출력
print(voting_clf.score(X_test, y_test))
# Soft Voting 사용
voting_clf.voting = "soft"
voting_clf.named_estimators["svc"].probability = True # SVC는 클래스 확률을 제공하지 않으므로 따로 지정 필요
voting_clf.fit(X_train, y_train)
print(voting_clf.score(X_test, y_test))
위 코드를 실행하면, 개별 모델보다 투표 기반 앙상블 모델에서 더 좋은 성능을 기록하는 것을 확인할 수 있다.
Stacking
앙상블에 속한 모든 학습기의 예측을 취합하는 과정에서, Voting 방식은 다수결 투표와 같은 간단한 함수를 사용한다.
그런데, “예측을 취합하는 모델을 훈련시킬 수는 없을까?”라는 아이디어에서 Stacking (스태킹) 방식이 등장하였다.

각 개별 학습기의 예측을 기반으로 최종 학습기 (Blender, Meta learner)가 예측을 수행한다.
이 때, 개별 학습기의 훈련 데이터로 최종 학습기의 훈련 데이터셋을 만들면 과적합의 우려가 있다. 대신, 교차 검증이나 홀드아웃을 사용해 개별 학습기 훈련에 사용되지 않은 샘플을 사용해야 한다.
사이킷런의 StackingClassifier 또는 StackingRegressor를 통해 스태킹을 구현할 수 있다.
from sklearn.ensemble import StackingClassifier
stacking_clf = StackingClassifier(
estimators=[ # 개별 학습기
('lr', LogisticRegression(random_state=42)),
('rf', RandomForestClassifier(random_state=42)),
('svc', SVC(probability=True, random_state=42))
],
final_estimator=RandomForestClassifier(random_state=43), # 최종 학습기
cv=5 # 교차 검증 폴드 개수
)
stacking_clf.fit(X_train, y_train)
위 코드에서 최종 학습기인 final_estimator 값을 지정하지 않은 경우, 분류는 LogisticRegression, 회귀는 RidgeCV를 사용한다.
다음은 앙상블 학습의 접근법으로, 크게 두 가지의 방식이 존재한다.
- Bagging : 개별 학습기 간 의존성이 약하며, 동시에 생성하는 병렬화 방식 (ex. Random Forest)
- Boosting : 개별 학습기 간 의존성이 강하며, 순차적으로 생성하는 연속화 방식 (ex. XGBoost, LightGBM)
본 포스팅에서는 Bagging (배깅)을 다루며, 2편에서는 Boosting을 설명한다.
Bagging
Bagging (배깅)은 Bootstrap Aggregating의 줄임말로, 훈련 데이터에서 반복적으로 복원 랜덤 샘플링(Bootstrap)하고 각 샘플 데이터에서 모델을 학습해 결과를 집계(Aggregate)하는 방법이다.

복원 랜덤 샘플링을 통해 데이터를 선택할 경우, 어떤 샘플은 여러 번 샘플링되지만, 어떤 것은 선택되지 않을 수 있다.
평균적으로 각 분류기에 훈련 샘플의 63.2%가 샘플링되며, 36.8%의 선택되지 않은 샘플은 OOB(Out-of-bag) 샘플로, 이는 검증 세트로 활용된다.
왜 63.2%인가?
m개의 샘플에서 랜덤으로 하나를 추출할 때, 선택되지 않을 확률은 (1−1m)이고, m번 반복하면 선택되지 않을 확률은 (1−1m)m이 된다. m을 무한대로 늘리면, 이 값은 로피탈의 정리에 의해 (e−1=0.3679)와 같아지게 된다. 따라서, 샘플이 선택될 확률은 1−0.3679=0.6321로 약 63.2%이다.
반면, 복원 추출하지 않고 랜덤 샘플링하는 Pasting (페이스팅) 방식도 존재하는데, 이 경우 모든 샘플을 고르게 사용할 수 있고, 이를 통해 개별 모델 간의 다양성이 증가되는 효과가 있다. 그러나, 배깅보다 과적합 위험성이 크고, 분산 감소 효과는 제한적이다.
사이킷런의 BaggingClassifier와 BaggingRegressor를 통해 배깅과 페이스팅을 구현할 수 있다.
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
bag_clf = BaggingClassifier(
DecisionTreeClassifier(),
bootstrap=True, # 배깅 : True(default), 페이스팅 : False
n_estimators=500, # 개별 의사결정나무 모델 500개 중에 (default : 10)
max_samples=100, # 랜덤 복원 방식으로 100개 추출 (default : 1)
n_jobs=-1, # 훈련과 예측에 사용할 CPU 코어 수 (-1은 모든 코어 사용, default : 1)
oob_score=True, # 훈련 종료 후 자동으로 OOB 평가 수행
random_state=42)
# 배깅 분류기 학습
bag_clf.fit(X_train, y_train)
# OOB 평가 정확도
print(bag_clf.oob_score_)
위 코드를 통해 배깅 분류기를 학습시킬 수 있고 OOB 평가도 가능하다.
Random Forest
Random Forest(랜덤 포레스트)는 배깅 방법을 적용한 앙상블 모델로, 다음의 2가지 특징을 가진다.
- 랜덤 속성 선택 방식 도입
- 개별 의사결정나무 활용
랜덤 속성 선택은 전체 d개의 속성 집합 중 랜덤으로 k개의 속성 부분집합을 선택하여 그 중 하나의 최적 속성으로 노드를 분할하는 방식이다. (일반적으로 k는 log2(d)를 사용한다.)
즉, 전체 특성 중 일부만 사용하겠다는 말이다.
일반적인 배깅 방식은 부트스트랩을 통해 훈련 샘플을 샘플링한다. 랜덤 포레스트는 훈련 특성과 샘플을 모두 샘플링하는 랜덤 패치 방식이다.
특성 샘플링은 더 다양한 예측기를 만들며, 편향을 늘리는 대신 분산을 낮춘다.
랜덤 포레스트는 사이킷런의 RandomForestClassifier 클래스를 활용해 구현할 수 있다.
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
iris = load_iris(as_frame=True)
rnd_clf = RandomForestClassifier(
n_estimators=500, # 500개의 개별 의사결정나무 활용
max_leaf_nodes=10, # 최대 리프 노드는 10개로 설정
n_jobs=-1,
random_state=42)
# BaggingClassifier 내 매개변수로 DecisionTreeClassifier 클래스를 선언해 동일하게 활용 가능
# rnd_clf = BaggingClassifier(
# DecisionTreeClassifier(
# max_features="sqrt",
# max_leaf_nodes=10),
# n_estimators=500, n_jobs=-1, random_state=42)
rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)
# 특성 중요도 출력
for score, name in zip(rnd_clf.feature_importances_, iris.data.columns):
print(round(score, 2), name)