HwangHub

[MLOPS] 샘플링, 의미 있는 행동인가? 본문

workspace/mlops

[MLOPS] 샘플링, 의미 있는 행동인가?

HwangJerry 2025. 3. 12. 22:43
TL;DR
샘플링 보다는 클래스 가중치로 불균형을 해소하는 것이 '일반적으로' 더 강력하다. 하지만 은탄환은 없으니 파이프라인을 구축하여 스코어를 비교해보는 것은 언제나 가치있는 일이다.

 
머신 러닝 모델을 만들 때 학습에 사용되는 데이터셋의 품질에 따라 성능이 크게 좌우된다는 것은 자명하다. 그렇다면 데이터의 분포가 불균형하다면 이를 어떻게 처리할 수 있을까.
 
해당 포스트에서는 SMOTE-Tomek 등의 샘플링 기법을 활용한 불균형 데이터에 대한 전처리 방법과 그 실효성에 대하여 탐구해본다.
 

데이터 샘플링

데이터 샘플링은 raw data로부터 sample data를 추출하는 기법을 말한다.
 
직관적으로 보통의 경우 '샘플링'이라는 단어를 마주하면 자연스럽게 전체 데이터 중 일부 데이터를 추출하여 샘플로 쓰는 것을 떠올릴 것이다. 이 것은 데이터 샘플링 기법 중 undersampling 이라 칭한다.
 
그렇다면 oversampling도 있다는 것인가? 그렇다. raw data를 기반으로 알고리즘을 통해 데이터를 추가하여 불균형을 해소하는 기법을 oversampling이라고 칭한다.
 
독자께서 공감하실지 모르겠지만, 데이터 샘플링을 처음 들었을 때에 개인적으로는 의심스러웠다. "데이터가 부족해서 학습을 못한다던데, undersampling을 한다고? 말도 안돼."라는 생각과 "oversampling으로 생성된 데이터가 오히려 noise가 되는 건 아닐까?"라는 생각이 머릿속을 지배했다.
 
결국 아래와 같은 결론에 도달한다.
 

샘플링 왜 함?

샘플링을 하는 이유는 데이터의 불균형이 심할 경우 모델의 성능에 큰 영향을 끼치기 때문이다. 사기 거래를 탐지하는 모델을 개발한다고 생각해보자. 대다수의 사람들이 사기를 칠까, 아니면 소수의 사람많이 사기를 칠까? (다행히도) 극소수의 사람들만이 사기 거래를 수행한다.

 
그렇다면 당연히 거래 데이터셋을 수집해보면 절대 다수는 '정상 거래'임을 쉽게 예상할 수 있다. 이러한 데이터를 그대로 학습한 모델은 거래가 발생했을 때 매우 높은 확률로 정상 거래라고 판단할 수 밖에 없다. 즉, 모델의 입장에서도 사기 거래에 대하여는 압도적으로 적은 표본으로 학습했기 때문에 '사기 거래는 극히 발생 확률이 드물다'라는 논리와 '어떤 형태가 사기 거래인지 충분하게 인지하지 못했다'라는 결과를 얻게 된다. 이렇게 학습된 모델은 '(majority class에) 과적합되었다.'라고 표현한다.
 
그렇기 때문에 샘플링이 필요하다. 샘플링을 수행하게 되면 매우 불균형한 데이터셋에 대하여도 어느 정도 균형을 유지하게 만들 수 있고, 이렇게 했을 때 모델의 성능이 더욱 향상되는 것을 확인할 수 있다. 즉, 사기 거래에 대한 데이터를 더욱 많이 학습하게 만들어서 '사기 거래'를 더욱 잘 검출할 수 있게 만든다는 것이다.
 

SMOTE-Tomek links 샘플링이란?

SMOTE-Tomek links 샘플링은 SMOTE와 Tomek links라는 두 가지 샘플링 기법을 합성한 샘플링 기법이다. SMOTE와 Tomek links를 각각 살펴본 후 SMOTE-Tomek links를 왜 사용하는지 이해해보자.
 

SMOTE

SMOTE는 현재 가장 많이 사용되는 oversampling 기법 중 하나이다. 기본적인 골자는 2002년 Nitesh Chawla 등이 발표한 SMOTE: Synthetic Minority Oversampling Technique 논문에서 확인할 수 있으며, 현재는 SMOTE의 단점인 over generalization을 보완한 Borderline SMOTE 또는 ADASYN 등의 변형 기법들 또한 많이 만들어졌다.

출처: baeldung

SMOTE는 minority class 데이터 중 무작위로 선택된 데이터와 가장 가까운 이웃 사이에 새로운 데이터를 생성하는 방식으로 oversampling을 수행한다. 이는 데이터를 생성할 때 majority를 고려하지 않고 생성하기 때문에 majority class와 minority class의 경계를 모호하게 만들 수 있고, 기존 데이터가 존재하는 noise를 기반으로 noise를 더 생성하여 데이터의 품질이 저하될 수 있는 우려가 존재한다. 따라서 이를 사용할 때에는 이러한 특징을 분명히 알고 사용해야 한다.
 

TOMEK-LINKS

Tomek-link는 서로 다른 클래스에 속하면서 서로 가장 가까운 이웃인 데이터의 쌍을 칭한다. 즉, tomek-links algorithm이라 함은 데이터셋의 클래스 간 경계에 존재하는 모호한 데이터 포인트를 제거하여 각 클래스 간의 분류를 명확하게 만드는 undersampling 기법을 의미한다.

출처: researchgate

이는 데이터의 모호성을 제거하고 노이즈를 감소시키는 데에 효과적인 기법이다. 하지만 데이터셋의 쌍별 거리를 모두 계산해야 하므로 계산 비용이 매우 높아 데이터셋의 크기에 따라 부담이 커질 수 있고, 단독으로 사용하기에는 그저 경계값에 존재하는 데이터 쌍만을 제거하는 것에 불과하여 그 효과가 크지 않다는 것이 특징이다.
 

SMOTE-Tomek links

SMOTE는 minority class의 데이터를 추가하여 학습에 필요한 데이터셋을 보완할 수 있지만 noise를 생성한다. 그리고 Tomek-links는 noise를 제거하는 효과를 가지고 있지만 단독으로 사용되기에는 그 효과가 약하다고 했다. 따라서 이 둘을 같이 쓴다면 그 시너지가 커질 수 있겠다라는 가정은 쉽게 도출된다. SMOTETomek은 먼저 SMOTE를 이용하여 데이터를 oversampling 한 뒤, Tomek links를 제거하는 과정을 통해 noise cleaning한다고 말한다.

좋다. 느낌이 오건데, oversampling이니 undersampling이니 이론상 둘 다 수행하는 SMOTETomek 기법이 가장 좋아 보인다. 실제로 수행해보면 SMOTE만 사용한 경우나 Tomek links만 사용한 경우보다 대개의 경우 SMOTETomek 이 더 좋은 성능을 보일 것만 같다....
 
진짜 SMOTETomek이 가장 좋을까?
 
실험하기 위해 LinearRegression 모델을 활용하여 binary classification 실험을 통한 성능 측정을 수행했다. 성능 지표는 precision, recall, 그리고 f1-score를 기준으로 한다.

# 불균형 데이터셋 생성 (클래스 비율 9:1)
X, y = make_classification(n_samples=10000, n_features=2, n_redundant=0,
                           n_clusters_per_class=1, weights=[0.9, 0.1],
                           random_state=42)

 
불균형 데이터셋은 편의상 생성하여 사용하였다. kaggle에 imbalance 라고 치면 많은 dataset이 나오므로 더 욕심있는 분이 있다면 응용해서 실험해봐도 좋을 듯 하다.
 
데이터셋은 보통 train, validation, test 세 부분으로 나누는 것이 일반적이지만, 편의를 위해 feature와 target 역할을 해줄 두 가지 X, y를 기준으로 train, test 두 개로 분리하였다. 비율은 무난하게 7:3으로 설정했다.

# 데이터셋 분할 (학습:테스트 = 7:3)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, test_size=0.3, random_state=42)

 
target trainset 기준으로 {0: 6276, 1: 724} 비율로 구성되었다. 확실하게 0이 majority class, 1이 minority class이다.

# 리샘플링 기법 적용
smote = SMOTE(random_state=42)
tomek = TomekLinks()
smote_tomek = SMOTETomek(random_state=42)

X_smote, y_smote = smote.fit_resample(X_train, y_train)
X_tomek, y_tomek = tomek.fit_resample(X_train, y_train)
X_smote_tomek, y_smote_tomek = smote_tomek.fit_resample(X_train, y_train)

 
학습 데이터셋에 대하여 세 가지 기법으로 resampling을 진행해 주었다. 참고로, test set은 오염되지 말아야 한다는 것이 rule of thumb이다.

SMOTE 적용 후 클래스 분포: Counter({0: 6276, 1: 6276})
Tomek Links 적용 후 클래스 분포: Counter({0: 6239, 1: 724})
SMOTETomek 적용 후 클래스 분포: Counter({0: 6137, 1: 6137})

 
위에서 다룬 것과 같이, SMOTE와 SMOTETomek은 수치상으로는 불균형이 확실하게 해결된 것을 확인할 수 있다. 그리고 Tomek links만을 적용한 train set은 불균형 자체는 해결되지 않았지만 원본과 비교했을 때 majority class의 개수가 줄어들었다. 모든 것은 예상했던 대로 움직이고 있다.

# 간단한 binary classification 모델인 Logistic Regression 사용
model = LogisticRegression(random_state=42, max_iter=1000)

 
간단하게 binary classification을 수행하기 위해 logistic regression 모델을 이용하였다.

# 성능 평가 함수 (Precision, Recall, F1-Score)
def evaluate_model(X_train, y_train, X_test, y_test, model_name):
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)

    # 성능 지표 계산
    precision, recall, f1, _ = precision_recall_fscore_support(y_test, y_pred, average='binary')

    print(f"\n{model_name} 모델 성능:")
    print(classification_report(y_test, y_pred))

    return {
        'name': model_name,
        'precision': precision,
        'recall': recall,
        'f1': f1
    }
    
# 각 모델 평가
results = []
results.append(evaluate_model(X_train, y_train, X_test, y_test, "원본 데이터"))
results.append(evaluate_model(X_smote, y_smote, X_test, y_test, "SMOTE"))
results.append(evaluate_model(X_tomek, y_tomek, X_test, y_test, "Tomek Links"))
results.append(evaluate_model(X_smote_tomek, y_smote_tomek, X_test, y_test, "SMOTETomek"))
# 결과 요약 테이블
results_df = pd.DataFrame({
    '모델': models,
    'Precision': [f"{p:.4f}" for p in precision_values],
    'Recall': [f"{r:.4f}" for r in recall_values],
    'F1-Score': [f"{f:.4f}" for f in f1_values]
})

print("\n결과 요약:")
print(results_df)

 
scikit-learn에서 지원하는 평가 지표 함수인 precision_recall_fscore_support를 이용하여 수치를 계산했다. 결과는 다음과 같다.

결과 요약:
           모델 Precision  Recall F1-Score
0     원본 데이터    0.9893  0.8935   0.9390
1        SMOTE    0.8936  0.9484   0.9202
2  Tomek Links    0.9893  0.8968   0.9408
3   SMOTETomek    0.9156  0.9452   0.9302

 
SMOTE 기법은 noise를 발생시킬 수 있으나 minority class의 개체수를 늘리는 기법이다. 따라서 위 지표에서도 알 수 있듯, 정확도는 다소 감소하는 경향을 보이지만 recall 지표는 증가하는 경향이 있음을 알 수 있다.(tradeoff 관계) 아울러 Tomek links는 단독으로 사용되었을 때 효과가 미미하지만 SMOTETomek과 SMOTE를 비교했을 때에는 SMOTETomek이 precision 스코어가 조금 더 높아질 수 있었던 것에 noise 제거 효과를 가지는 Tomek links가 영향을 끼쳤을 거라고 유추해볼 수 있다.
 

그럼, SMOTETomek과 같은 혼합 샘플링을 잘 쓰면 되려나?

위 실험을 하기 까지는 위와 같이 생각했다. 하지만 조사를 하면 할수록 이상한 점을 발견했다. 논문을 찾아보면 SMOTE를 활발히 다뤄왔음을 느낄 수 있지만, 실제 kaggle competition에서 우승한 기록 중 Sampling 기법을 사용한 사례가 보이지 않는다는 것. 이러한 의문은 나 뿐만 아니라 다른 이들도 진작 제기한 바 있다. 이유를 탐구해보니, 잘 설명해주는 논문이 발견되었다.
 

TO SMOTE, or NOT TO SMOTE

논문을 보면, 최근 ML 모델링을 할 때 주로 사용되는 앙상블 기법 기반의 RandomForest, 그리고 XGBoost 등의 트리 기반 알고리즘의 경우 클래스 불균형을 가중치를 활용하여 해결하기 때문에 불균형 데이터셋에서도 충분한 성능을 보인다고 결론을 지었다. 이는 kaggle 대회에서 왜 샘플링 기법이 등장하지 않는지를 설명하는 대표적인 실험 중 하나로 보인다. 외국의 개발자들은 이를 보며 샘플링 기법이 사용되지 않는 이유를 한 마디로 정의한다. "Because, it doesn't work."
 
XGBoost를 예로 들자면, 클래스 가중치를 아래와 같이 적용할 수 있다.

scale_pos_weight = np.sum(y_train == 0) / np.sum(y_train == 1)

xgb_weighted = xgb.XGBClassifier(
    scale_pos_weight=scale_pos_weight,  # 양성 클래스 가중치
    learning_rate=0.1,
    n_estimators=100,
    max_depth=5,
    random_state=42
)

 
산술적으로 합리적인 가중치를 손쉽게 적용하여 학습할 수 있으며, 결정 트리 구조상 stratified하게 데이터셋을 활용하며 학습할 수 있기 때문에 불균형에 크게 좌우되지 않고 좋은 성능을 보이는 것으로 이해해볼 수 있다.
 

그렇다면 샘플링은 전혀 의미가 없는 행위일까?

 
완전히 그런 것은 아니다. Uppsala University에서 진행한 연구에 따르면, SMOTE와 ADASYN의 성능을 비교하고자 하였는데, 이 실험에서 우리는 샘플링이 전혀 의미없는 행위는 아님을 알 수 있다. 해당 연구에서는 서로 다른 불균형을 가진 데이터셋 3개를 활용한다.

출처: A Comparative Review of SMOTE and ADASYN in Imbalanced Data Classification (Uppsala University, 2020)

 
위 자료를 보면, 각각의 데이터셋의 불균형 정도가 큰 폭으로 달라짐을 알 수 있다. 이를 기반으로 SVM, Linear Regression, 그리고 RandomForest 모델을 기반으로 score를 매기면서 실험이 진행된다.

출처: A Comparative Review of SMOTE and ADASYN in Imbalanced Data Classification (Uppsala University, 2020)

 
RandomForest 기준으로는, 결론적으로 역시나 resampling 유무에 따른 성능 차이가 크지 않음을 보여준다.  이는 RF의 구조가 여러 개의 의사 결정 트리로 구성되어 각 트리가 샘플 데이터를 기반으로 학습하는 것으로 되어 있기 때문인 것으로 보인다. 다만, fraud detetion 과 같이 극단적인 imbalance에 대하여는 소폭 성능 향상을 보여주었다.

출처: 본인 코드

RF의 경우 역시나 SMOTE를 진행하기 전과 후의 결정 경계가 크게 다르지 않음을 볼 수 있다. 이는 앞서 말한 것과 같이, 결정 트리 구조 특성상 데이터셋의 균형 정도가 모델의 성능에 큰 영향을 끼치기 어렵다는 것을 알 수 있다.
 
반면, SVM이나 LinearRegression 모델에 대하여는 유무에 따라 차이가 꽤나 크게 발생한 것을 알 수 있다. 특히나 SVM의 경우 모델이 판단을 수행하는 결정 경계를 변화시키며 동작하게 되는데, 데이터가 imbalance할 수록 그 경계를 설정하는 것에 큰 영향을 받는 것으로 보인다.

출처: 본인 코드

위에서 볼 수 있듯이, SMOTE를 적용했을 때 더욱 넓은 범위로 결정 경계를 가져가는 것을 확인할 수 있다.

 
선형 회귀 모델의 경우에도 마찬가지로 큰 폭으로 변환하지만, 결정 경계를 선형으로 가져가기 때문에 불균형이 너무 심한 경우에는 오히려 sampling을 수행하는 게 역효과를 일으킬 수도 있음을 해당 실험을 통해 알 수 있었다.
 

결론

최근에 좋은 성능을 보이는 ML 모델은 부스팅 앙상블 기법을 적용한 모델들이다. 이러한 모델들은 대개 별도의 샘플링 기법을 적용하지 않고도 좋은 성능을 보이고 있어 실제 비즈니스에 사용될 모델을 개발할 경우에 sampling은 중요한 아젠다가 되지 않을 가능성이 높다.
 
하지만 언제나 ML은 블랙박스의 경향을 갖고 있으며, 직접 실험해봤을 때 예상한 것과 다를 수 있으므로 MLOPS 시스템을 구축할 때 sampling 파이프라인을 구축하는 것 자체가 무의미해 보이진 않는다는 것이 개인적인 결론이다. 즉, 언제나 silver bullet은 없으므로 항상 여러 방법을 테스트하고 비교하는 실험적인 접근법이 필요하며, (개인적으로 느끼기에) 수많은 실험적 접근을 자동화할 수 있는 MLOps 시스템의 중요성이 다시금 느껴지는 공부였다.
 

참고:

https://arxiv.org/pdf/2201.08528
https://www.diva-portal.org/smash/get/diva2:1519153/FULLTEXT01.pdf
https://datascience.stackexchange.com/questions/106461/why-smote-is-not-used-in-prize-winning-kaggle-solutions

 

Comments