이번에는 지난번 포스팅에 이어서 불균형 데이터 처리에 대해서 알아보도록 하겠습니다.
[머신러닝 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하겠습니다.
댓글