불균형 데이터 분류에서 SMOTE·비용 민감 학습·EasyEnsemble 선택법
불균형 데이터 분류에서 SMOTE, 비용 민감 학습, EasyEnsemble의 특성과 검증·임계값 운영 전략을 정리합니다.
2026-08-14 · 최초 발행 2025-10-14
불균형 데이터에서는 다수 클래스를 잘 맞히는 모델이 실제로 중요한 소수 클래스를 놓칠 수 있다. 보안 탐지, 금융 사기, 의료 진단처럼 놓침과 오탐의 비용이 다른 문제에서는 정확도 편향, 임계값 미스매치, 데이터 누수가 운영 판단을 왜곡한다.
SMOTE, Cost-Sensitive Learning, EasyEnsemble은 이 문제를 서로 다른 방식으로 다룬다. 선택 기준은 데이터 분포만이 아니라 오분류 비용과 운영 제약까지 포함한다.
소수 클래스를 놓치는 분류 문제
불균형 데이터는 다수 클래스에 비해 소수 클래스 비율이 현저히 작은 분류 문제다. 이 조건에서는 Accuracy가 높은 모델도 소수 클래스 성능이 낮을 수 있다.
SMOTE(Synthetic Minority Over-sampling Technique)는 소수 클래스 샘플 사이에서 k-NN 기반 보간을 수행해 합성 샘플을 만든다. 경계 부근의 데이터 밀도를 넓혀 클래스 분리 가능성을 높이는 방식이다.
Cost-Sensitive Learning은 오분류 비용을 학습 목표에 넣는다. 클래스 가중치나 손실함수 커스터마이징으로 FN과 FP의 비대칭 비용을 반영한다.
EasyEnsemble는 다수 클래스를 여러 차례 언더샘플링한 뒤, 각 서브셋에서 약분류기를 학습하고 앙상블한다. 다수 클래스 정보를 한 번에 버리는 문제를 줄이고 분산을 낮추는 접근이다.
데이터 경계, 비용, 앙상블을 다루는 방식
SMOTE는 소수 클래스 샘플을 입력으로 받아 k-NN 이웃을 고르고, 그 사이를 무작위로 보간해 합성 소수 클래스 샘플을 생성한다. 경계 주변의 데이터 밀도를 확장할 수 있지만, k_neighbors ≤ (소수 클래스 수 − 1) 조건은 확인해야 한다. 고차원 데이터나 잡음이 많은 데이터에서는 Borderline-SMOTE, SMOTE-NC, ADASYN 같은 변형 기법을 검토할 수 있다.
리샘플링은 반드시 훈련 데이터 Fold 내부에서만 실행해야 한다. 교차검증 이전에 전체 데이터에 SMOTE를 적용하면 데이터 누수가 발생하므로 Pipeline 기반 구성이 필요하다.
비용 민감 학습은 원래 데이터 분포를 바꾸지 않는다는 점이 특징이다. FN 비용이 FP보다 큰 문제에서는 클래스 가중치나 비용 행렬을 적용하고, 후단의 임계값 최적화까지 연결해 비즈니스 비용을 낮출 수 있다. 교차검증 과정에서 비용 기대값이 최소가 되는 임계값을 찾는 절차가 적합하다. 다만 극단적인 불균형에서는 학습 수렴성과 표현력에 제약이 생길 수 있다.
EasyEnsemble는 다수 클래스 반복 언더샘플링, 각 서브셋의 약분류기 학습, 소프트 또는 하드 보팅 앙상블 순서로 동작한다. 병렬화에 유리하고 서브샘플의 다양성이 경계 안정성을 높일 수 있다. 반면 데이터가 매우 크면 시간과 메모리 비용이 증가하며, 베이스 러너가 지나치게 복잡하면 과적합 위험이 있다.
검증과 배포까지 이어지는 흐름
평가에서는 ROC-AUC, PR-AUC, F1, Recall, MCC를 함께 보고 운영 임계값은 별도로 평가한다. 소수 클래스가 중요한 문제에서는 PR-AUC가 특히 유용하다.
검증은 Stratified K-Fold로 분할한 뒤 각 Fold 안에서 스케일링, 리샘플링, 학습을 수행하고 메트릭 평균과 분산을 점검하는 순서가 적절하다. 데이터 누수 방지가 우선이다.
비용 행렬 C_fp, C_fn을 정한 다음 기대비용을 최소화하는 임계값을 선택하고, A/B 테스트 또는 챔피언-도전자 방식으로 배포할 수 있다.
문제 성격에 따른 적용 장면
카드 사기 탐지에서는 Cost-Sensitive Learning과 비용 최적화 임계값을 함께 적용할 수 있다. C_fn(사기 미검출) ≫ C_fp(정상 오탐)이 되도록 가중치를 두고, PR-AUC를 우선 지표로 사용한다. Recall@Precision≥X% 제약을 두며 경계급수 변동이 있으면 분기별 재교정을 수행한다.
제조 불량 검출에서는 EasyEnsemble로 클래스 경계의 다양성을 확보할 수 있다. 센서 노이즈가 있으면 의사결정나무나 라이트GBM depth 제한처럼 베이스 러너를 단순화하는 방법을 고려한다. 라인별 분포 차이에는 도메인 적응과 데이터 드리프트 감시가 필요하다.
희귀 질환 진단에서는 범주형 변수를 포함할 수 있는 SMOTE-NC와 로지스틱 회귀 또는 선형 SVM 조합을 사용할 수 있다. 이 경우에는 해석 가능성을 중시하며, 환자 안전 관점에서 Recall을 우선하고 임계값을 후속 검사 트리거가 되는 진료 플로우와 연결한다.
성능 기대와 방법별 성격
데이터와 도메인에 따라 PR-AUC는 2060% 상대 개선, Recall은 1030%p 향상, FN 기대비용은 30%+ 절감될 수 있다. 임계값과 비용을 맞추면 비즈니스 판단의 일관성이 높아지고, Fold 내부 처리 원칙을 정착시키면 재현성도 개선된다. 앙상블은 경계 안정성을 높이는 데 기여한다.
| 항목 | SMOTE | Cost-Sensitive | EasyEnsemble |
|---|---|---|---|
| 성능(소수 클래스) | 경계 확장으로 Recall↑, 노이즈 민감 | 비용 반영으로 균형적, 극단 불균형 시 한계 | 다양성으로 견고성↑, 종합 성능 우수 |
| 확장성 | 대용량 시 시간/메모리 부담 | 매우 우수, 추가 샘플링 없음 | 중간~높음, n_estimators에 비례 |
| 일관성 | 랜덤성 존재, 고정 seed 필요 | 높음, 결정경계 안정 | 높음, 다만 베이스 러너 변동성 존재 |
| 안정성 | 과적합/노이즈 위험, 변형 기법 필요 | 과적합 위험 낮음 | 과적합 억제, 파라미터 과다 시 위험 |
| 운영 편의 | 파이프라인 관리 필요 | 가장 단순, 해석 용이 | 설정/모니터링 비용 중간 |
데이터 규모가 크고 비용 정보가 명확하면 Cost-Sensitive Learning을 우선 적용할 수 있다. 경계가 비선형이고 희소하다면 SMOTE 변형과 단순 모델 조합이 맞고, 데이터 다양성과 강건성이 중요하면 EasyEnsemble가 후보가 된다.
전처리, 리샘플링, 학습은 Fold 내부에 캡슐화해야 한다. PR 곡선으로 임계값을 탐색하고 비용 행렬로 검증해 운영 KPI와 맞춘다. 사전확률과 캐리어 특성의 변화를 감시하며, 성능 저하가 확인되면 재학습과 재교정을 진행한다. 앙상블에는 리소스 비용 증가라는 트레이드오프가 따른다.
재현 가능한 비교 코드
전제조건
- Python 3.10+, scikit-learn ≥ 1.3, imbalanced-learn ≥ 0.12
- 설치: pip install scikit-learn imbalanced-learn
# Python 3.10+, scikit-learn >=1.3, imbalanced-learn >=0.12
from sklearn.datasets import make_classification
from sklearn.model_selection import StratifiedKFold, cross_validate, train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (roc_auc_score, average_precision_score, f1_score, recall_score,
precision_recall_curve)
from sklearn.pipeline import Pipeline
from imblearn.over_sampling import SMOTE
from imblearn.ensemble import EasyEnsembleClassifier
import numpy as np
RANDOM_STATE = 42
# 1) 데이터 생성(불균형 시나리오)
X, y = make_classification(n_samples=20000, n_features=30, n_informative=10, n_redundant=5,
weights=[0.98, 0.02], flip_y=0.001, class_sep=1.0,
random_state=RANDOM_STATE)
# 2) 공통 검증 설정
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=RANDOM_STATE)
scoring = {'roc_auc': 'roc_auc', 'pr_auc': 'average_precision', 'f1': 'f1', 'recall': 'recall'}
# 3) Cost-Sensitive 파이프라인(원데이터 + 클래스 가중)
cost_sensitive = Pipeline([
('scaler', StandardScaler()),
('clf', LogisticRegression(class_weight='balanced', max_iter=200, n_jobs=None,
random_state=RANDOM_STATE))
])
# 4) SMOTE 파이프라인(훈련 Fold 내부에서만 오버샘플링)
minority_count = np.sum(y == 1)
k_neighbors = max(1, min(5, minority_count - 1)) # 안전 가드
smote_lr = Pipeline([
('scaler', StandardScaler()),
('smote', SMOTE(k_neighbors=k_neighbors, random_state=RANDOM_STATE)),
('clf', LogisticRegression(max_iter=200, n_jobs=None, random_state=RANDOM_STATE))
])
# 5) EasyEnsemble 분류기(언더샘플링 앙상블)
eec = EasyEnsembleClassifier(n_estimators=10, random_state=RANDOM_STATE, n_jobs=-1)
# 6) 교차검증 비교
for name, model in [('CostSensitive', cost_sensitive), ('SMOTE+LR', smote_lr), ('EasyEnsemble', eec)]:
cv_res = cross_validate(model, X, y, cv=cv, scoring=scoring, n_jobs=-1, return_train_score=False)
print(f"[{name}] ROC-AUC={cv_res['test_roc_auc'].mean():.4f} "
f"PR-AUC={cv_res['test_pr_auc'].mean():.4f} "
f"F1={cv_res['test_f1'].mean():.4f} Recall={cv_res['test_recall'].mean():.4f}")
# 7) 비용 기반 임계값 튜닝(검증 셋)
X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.2,
stratify=y, random_state=RANDOM_STATE)
# 예: SMOTE + LR로 학습
smote_lr.fit(X_train, y_train)
proba = smote_lr.predict_proba(X_valid)[:, 1]
# 비용 행렬 설정(도메인에 맞게 수정)
C_fp = 1.0 # 정상인데 사기로 판단 비용
C_fn = 20.0 # 사기인데 정상으로 놓치는 비용
prec, rec, thr = precision_recall_curve(y_valid, proba)
# 기대비용 최소 임계값 탐색: E[cost] = C_fp * FP + C_fn * FN
# PR커브 기준 대신, 실제 임계값 후보를 확률 기반으로 스캔
candidates = np.linspace(0.01, 0.99, 99)
best_t, best_cost = 0.5, float('inf')
for t in candidates:
y_hat = (proba >= t).astype(int)
FP = np.sum((y_hat == 1) & (y_valid == 0))
FN = np.sum((y_hat == 0) & (y_valid == 1))
cost = C_fp * FP + C_fn * FN
if cost < best_cost:
best_cost, best_t = cost, t
print(f"최적 임계값(비용 최소): {best_t:.3f}, 기대비용: {best_cost:.1f}")
print(f"ROC-AUC={roc_auc_score(y_valid, proba):.4f}, PR-AUC={average_precision_score(y_valid, proba):.4f}")
y_hat_opt = (proba >= best_t).astype(int)
print(f"F1={f1_score(y_valid, y_hat_opt):.4f}, Recall={recall_score(y_valid, y_hat_opt):.4f}")
리샘플링은 훈련 Fold 내부에서 수행하고 Pipeline으로 묶는다. k_neighbors는 소수 클래스 크기에 맞춰 안전하게 설정한다. EasyEnsemble에는 n_estimators와 베이스 러너 복잡도 사이의 트레이드오프가 있다. 임계값은 비용 행렬을 기준으로 검증 세트에서 최적화해 고정한 뒤, 배포 후 주기적으로 재교정한다.