본문 바로가기
머신러닝 with Python

[머신러닝 with Python] 불균형 데이터 처리(3) : TomekLink활용

by CodeCrafter 2024. 11. 23.
반응형

이번에는 지난번 포스팅에 이어서 불균형 데이터 처리에 대해서 알아보도록 하겠습니다.

 

[머신러닝 with Python] 불균형 데이터 처리(2) : 불균형 클래스 분류 문제 평가지표

 

이번에 알아볼 것은 TomekLink라는 기법입니다.

 

1. Tomek Link란?

- Tomke Link는 데이터셋의 클래스 불균형을 줄이기 위해 언더샘플링을 하는 방식 중 하나로, 주로 이진 분류(Binary Classification)에서 사용됩니다.

 

- 이는, 이상치나 경계에 위치한 샘플을 제거하여 두 클래스 간의 경계를 더 명확하게 만드는 것인데요

 * 두 데이터 포인트 사이의 가까운 쌍을 기반으로 작동하며, 만약 두 포인트 A, B가 서로 다른 클래스에 속하고, 다른 데이터 포인트들보다 서로 더 가까운 경우, 이 두 포인트를 Tomek Link라 정의합니다. 

 * 이 Tomek Link의 존재는 두 클래스의 경계에 노이즈나 이상치가 있다는 것을 의미하게 됩니다.

 

- Tomek Link를 활용한 언더샘플링 방법은 아래와 같이 진행됩니다.

 * 각 샘플에 대해 가장 가까운 이웃을 찾기 (유클리디안 거리를 활용합니다)

 * Tomek Link 쌍 식별 : 서로 다른 클래스에 속하면서, 서로가 가장 가까운 이웃인 샘플 쌍이 Tomek Link가 됩니다.

 * 언더샘플링 수행 : Tomek Link 쌍에 속한 샘플 중 소수 클래스가 아닌 다수 클래스 샘플을 제거하여 두 클래스 사이의 경계가 더 명확해지고, 불균형도 어느정도 해소되게 만듭니다.

출처 : https://gdsc-university-of-seoul.github.io/anomaly-detection/

 

 

- Tomek Link의 장점과 한계

 1) 장점 

 1-a) 경계상의 노이즈 제거 : Tomek Link는 클래스 경계에 있는 노이즈나 이상치 데이터를 제거해 모델이 경계를 더 정확하게 학습할 수 있도록 도와줍니다.

 1-b) 클래스 불균형 개선 : 다수 클래스의 샘플의 일부를 제거하여 클래스간의 비율을 조정해주어 학습에 도움을 줄 수 있습니다.

 1-c) 다른 샘플링 방법과의 조합: Tomek Link는 SMOTE와 같은 오버 샘플링 기법과 함께 사용될 수 있으며 이를 통해 더 균형 잡힌 데이터셋을 만들 수 있습니다.

 

 2) 단점

 2-a) 데이터 손실 : 언더 샘플링을 하기에 클래스 구분에 중요한 데이터가 담긴 샘플을 없애버릴 수 도 있습니다. 

 2-b) 복잡한 경계 처리에는 한계가 있음 : 단순히 유클리디안 거리 상에서 가까운 이웃을 기준으로 Tomek Link를 만들기에 차원이 높고 복잡한 데이터에 대해서는 잘 작동하지 않을 수도 있습니다.

 

 

2. 분류 성능 비교 (Tomek Link + XGBoost)

- 지난번에 XGBoost를 활용해서 CreditCardFraudDetection 데이터에 대해서 분류를 진행했었습니다. 분류결과는 

이와 같이 나왔습니다.

 

- 이번에는 Tomek Link를 활용해보겠습니다.

 

- 먼저 데이터를 불러오겠습니다. 데이터는 동일하게 CreditCardFraudDetection입니다.

# 필요한 라이브러리들 임포트
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from imblearn.under_sampling import TomekLinks
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# 데이터 로드
data = fetch_openml(data_id=44235, as_frame=True)
df = pd.DataFrame(data.data, columns=data.feature_names)
df['Class'] = data.target.astype(int)

# 입력과 타겟 분리
X = df.drop('Class', axis=1)
y = df['Class']

 

- 이후 Tomek Link 적용 전과 후에 대해서 비교해보겠습니다.

 * 먼저 적용 전입니다. 

# Standard Scaling 적용
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# Scaling 후 데이터 불균형 확인
plt.figure(figsize=(8, 4))
sns.countplot(data=df, x='Class')
plt.title("Class Distribution Before Tomek Links")

# 막대 위에 개수와 비율 표시
total = len(y)
for p in plt.gca().patches:
    height = p.get_height()
    percentage = f'{height} ({height/total:.1%})'
    plt.text(p.get_x() + p.get_width() / 2., height + 5, percentage, ha="center")

plt.show()

 

* 다음은 적용후입니다.

# Tomek Link 적용하여 데이터 균형 맞추기
tomek = TomekLinks()
X_res, y_res = tomek.fit_resample(X_scaled, y)

# Tomek Link 적용 후 데이터 분포 시각화
df_res = pd.DataFrame(X_res, columns=X.columns)
df_res['Class'] = y_res

plt.figure(figsize=(8, 4))
sns.countplot(data=df_res, x='Class')
plt.title("Class Distribution After Tomek Links")

# 막대 위에 개수와 비율 표시
total_res = len(y_res)
for p in plt.gca().patches:
    height = p.get_height()
    percentage = f'{height} ({height/total_res:.1%})'
    plt.text(p.get_x() + p.get_width() / 2., height + 5, percentage, ha="center")

plt.show()

* Tomek Link 적용 전 0 class의 개수가 284,315 이고, 적용 후가 284,295이니 20개가 감소함을 알 수 있습니다. 이렇게 20개를 제거하고 나서 과연 어떤 학습에 긍정적 영향을 미쳤을지 확인해보겠습니다.

 

- 모델링은 XGBoost이고, Train과 Test는 8:2로 분리했습니다. 

 

- 먼저 Tomek Link적용 전입니다.

 * 5fold cv로 학습하였고, 결과는 아래와 같습니다.

# 필요한 라이브러리 임포트
from sklearn.model_selection import train_test_split, StratifiedKFold
from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score, average_precision_score, matthews_corrcoef, cohen_kappa_score
from tqdm import tqdm
import numpy as np


# 훈련, 테스트 세트 분리 (훈련 80%, 테스트 20%)
train_df, test_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df['Class'])
X_train, y_train = train_df.drop(columns=['Class']), train_df['Class']
X_test, y_test = test_df.drop(columns=['Class']), test_df['Class']

# Standard Scaling 적용
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# XGBoost 모델 정의
model = XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42)

# 5-fold 교차 검증을 위한 초기 설정
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv_metrics = {
    "Accuracy": [],
    "Precision": [],
    "Recall": [],
    "F1 Score": [],
    "AUROC": [],
    "AUPRC": [],
    "MCC": [],
    "Kappa": []
}

# 5-fold 교차 검증
for train_idx, val_idx in tqdm(skf.split(X_train_scaled, y_train), total=5, desc="5-Fold Cross-Validation"):
    X_train_fold, X_val_fold = X_train_scaled[train_idx], X_train_scaled[val_idx]
    y_train_fold, y_val_fold = y_train.iloc[train_idx], y_train.iloc[val_idx]
   
    # 모델 학습 및 예측
    model.fit(X_train_fold, y_train_fold)
    y_val_pred = model.predict(X_val_fold)
    y_val_proba = model.predict_proba(X_val_fold)[:, 1]  # 확률 예측 사용 (AUROC, AUPRC 계산에 사용)
   
    # 각 폴드의 평가지표 계산
    cv_metrics["Accuracy"].append(accuracy_score(y_val_fold, y_val_pred))
    cv_metrics["Precision"].append(precision_score(y_val_fold, y_val_pred))
    cv_metrics["Recall"].append(recall_score(y_val_fold, y_val_pred))
    cv_metrics["F1 Score"].append(f1_score(y_val_fold, y_val_pred))
    cv_metrics["AUROC"].append(roc_auc_score(y_val_fold, y_val_proba))
    cv_metrics["AUPRC"].append(average_precision_score(y_val_fold, y_val_proba))
    cv_metrics["MCC"].append(matthews_corrcoef(y_val_fold, y_val_pred))
    cv_metrics["Kappa"].append(cohen_kappa_score(y_val_fold, y_val_pred))

# 각 지표의 평균을 계산하여 저장
metrics_summary = {f"{metric} Mean": np.mean(values) for metric, values in cv_metrics.items()}
metrics_summary.update({f"{metric} Std": np.std(values) for metric, values in cv_metrics.items()})

# 결과 출력
results_df = pd.DataFrame(metrics_summary, index=["XGBoost"])
results_df

 

* 이제 이렇게 학습된 모델을 가지고 Test 데이터에 적용해보았습니다.

# 전체 훈련 데이터로 모델 훈련 후 테스트 세트에서 평가
model.fit(X_train_scaled, y_train)
y_test_pred = model.predict(X_test_scaled)
y_test_proba = model.predict_proba(X_test_scaled)[:, 1]

# 테스트 세트에 대한 평가지표 계산
test_metrics = {
    "Accuracy": accuracy_score(y_test, y_test_pred),
    "Precision": precision_score(y_test, y_test_pred),
    "Recall": recall_score(y_test, y_test_pred),
    "F1 Score": f1_score(y_test, y_test_pred),
    "AUROC": roc_auc_score(y_test, y_test_proba),
    "AUPRC": average_precision_score(y_test, y_test_proba),
    "MCC": matthews_corrcoef(y_test, y_test_pred),
    "Kappa": cohen_kappa_score(y_test, y_test_pred)
}

# 테스트 세트 결과 출력
test_results_df = pd.DataFrame(test_metrics, index=["Test Set"])
print("\nTest Set Results:")
test_results_df

 

 

- 다음은 Tomek Link 적용 후입니다.

* 먼저 5-fold CV입니다.

# XGBoost 모델 정의
model = XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42)

# 5-fold 교차 검증을 위한 초기 설정
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
cv_metrics = {
    "Accuracy": [],
    "Precision": [],
    "Recall": [],
    "F1 Score": [],
    "AUROC": [],
    "AUPRC": [],
    "MCC": [],
    "Kappa": []
}

# 5-fold 교차 검증
for train_idx, val_idx in tqdm(skf.split(X_res, y_res), total=5, desc="5-Fold Cross-Validation"):
    X_train_fold, X_val_fold = X_res[train_idx], X_res[val_idx]
    y_train_fold, y_val_fold = y_res.iloc[train_idx], y_res.iloc[val_idx]
   
    # 모델 학습 및 예측
    model.fit(X_train_fold, y_train_fold)
    y_val_pred = model.predict(X_val_fold)
    y_val_proba = model.predict_proba(X_val_fold)[:, 1]  # 확률 예측 사용 (AUROC, AUPRC 계산에 사용)
   
    # 각 폴드의 평가지표 계산
    cv_metrics["Accuracy"].append(accuracy_score(y_val_fold, y_val_pred))
    cv_metrics["Precision"].append(precision_score(y_val_fold, y_val_pred))
    cv_metrics["Recall"].append(recall_score(y_val_fold, y_val_pred))
    cv_metrics["F1 Score"].append(f1_score(y_val_fold, y_val_pred))
    cv_metrics["AUROC"].append(roc_auc_score(y_val_fold, y_val_proba))
    cv_metrics["AUPRC"].append(average_precision_score(y_val_fold, y_val_proba))
    cv_metrics["MCC"].append(matthews_corrcoef(y_val_fold, y_val_pred))
    cv_metrics["Kappa"].append(cohen_kappa_score(y_val_fold, y_val_pred))

# 각 지표의 교차 검증 평균을 계산하여 저장
metrics_summary = {f"{metric} Mean": np.mean(values) for metric, values in cv_metrics.items()}
metrics_summary.update({f"{metric} Std": np.std(values) for metric, values in cv_metrics.items()})

# 교차 검증 결과 출력
results_df = pd.DataFrame(metrics_summary, index=["XGBoost"])
print("5-Fold Cross-Validation Results with Tomek Link Applied:")
results_df

 

* 이제 Test 데이터에 대해 적용해보겠습니다.

# Tomek Link 적용된 훈련 데이터로 모델 재훈련 후 테스트 세트에서 평가
model.fit(X_res, y_res)
y_test_pred = model.predict(X_test_scaled)
y_test_proba = model.predict_proba(X_test_scaled)[:, 1]

# 테스트 세트에 대한 평가지표 계산
test_metrics = {
    "Accuracy": accuracy_score(y_test, y_test_pred),
    "Precision": precision_score(y_test, y_test_pred),
    "Recall": recall_score(y_test, y_test_pred),
    "F1 Score": f1_score(y_test, y_test_pred),
    "AUROC": roc_auc_score(y_test, y_test_proba),
    "AUPRC": average_precision_score(y_test, y_test_proba),
    "MCC": matthews_corrcoef(y_test, y_test_pred),
    "Kappa": cohen_kappa_score(y_test, y_test_pred)
}

# 테스트 세트 결과 출력
test_results_df = pd.DataFrame(test_metrics, index=["Test Set"])
print("\nTest Set Results with Tomek Link Applied:")
test_results_df

 

 

결과가 확연히 좋아진 것을 알 수 있습니다.

 

 

 ** 다만 실험 전에 Train Test split을 한 뒤에 Tomek 링크를 Train 데이터에만 적용해야 하는데 Tomek Link를 적용한 뒤 Train Test Split을 했기에 Fair 하지는 않은 결과인것 같습니다. 그렇지만, Tomek Link가 분류 경계선상의 애매한 Majority 데이터들을 제거하여 학습 자체는 잘되었음을 알 수 있습니다(Overfitting 가능성도 내재..)

** 다음 포스팅에서 진행할 실험에서는 먼저 Split을 하고 실험을 진행하여 이번에 부족한 부분도 다시 Revision하겠습니다.

반응형

댓글