Kaggle Credit Card Fraud Detection 대회에 참여하며 이진 분류 문제를 해결할 때 기본적으로 세팅하고 가면 좋을 로직을 정리해 보았다. 참고로, 모델링 시 추가적으로 활용할 수 있는 소소한 팁들은 이후에 포스팅할 Regression: 회귀 문제 구조화하기에서 함께 다룰 예정이다.
이 글에서는 전처리나 분석 과정보다는 불균형 데이터 이진 분류 문제를 해결하는 과정에서 도출할 수 있었던 기술적인 인사이트를 위주로 하고 있다.
특히 XGBoost와 LightGBM에서 조기 종료를 수행하는 파라미터 작성법이 달라 이 부분도 오류 없이 동작하도록 정리하였다.
평가 지표는 ROC-AUC를 사용하는 것을 전제로 수행한다.
중간에는 불균형 데이터에서 헷갈리기 쉬운 성능 지표 해석 Tip 도 함께 담아 보았다.
글을 작성하게 된 이유를 간단하게 언급하고 넘어가겠다.
지금까지 캐글 필사를 해오고 있었지만 실제로 열려 있는 대회에 처음부터 끝까지 직접 로직을 작성해서 참여해 본 적은 없었다. 이번에 분류 예측을 수행하는 과정은, 앞으로 비슷한 문제를 해결하는 방향이 될 수 있도록 각 단계를 함수와하여 구조화하는 시간이 되었다.
사실 모델링하고 예측 성능을 올리는 시간보다 각 단계를 정의하고 구조화하는 데에 더 많은 시간을 쓴 것 같다. 이렇게 정리한 구조를 기반으로 이후 다양한 문제를 해결하면서 고도화하는 것을 목표로 한다.
해당 문제를 해결하면서 정의한 함수를 포함한 자세한 모델링 과정과 전처리 로직은 GitHub repository에서 확인할 수 있다. 더 효율적인 방법이 있거나, 왜 이렇게 구현한 건지 의문이 드는 부분이 있다면 언제든 알려주기 바란다 :)
해당 문제를 해결하는 노트북은 다음과 같은 목차로 구성하였다.
Fine-tuning이나 Stanking 등을 적용하지 않은 단순한 구조이지만, 클래스 불균형 문제를 가진 이진 분류 데이터를 예측할 때 베이스로 가져가면 좋을 것 같아 정리해 두었다. 데이터 전처리 및 튜닝 과정을 상황에 맞게 적절히 변형하여 선택적으로 사용할 수 있다.
- 데이터 로드 및 확인
1.1 라이브러리 및 데이터 불러오기
1.2 데이터 확인
1.3 타겟 데이터 확인
1.4 결측치 및 이상치 확인
- 데이터 전처리
2.1 전처리 함수 정의
2.2 Train/Test 데이터 분리
2.3 SMOTE 적용
2.4 평가 함수 정의
- 모델 비교 및 선택
3.1 LogisticRegression
3.2 LightGBM
3.3 XGBoost
3.4 RandomFores
3.5 DecisionTree
3.6 KNN
3.7 AdaBoost
3.8 Baseline 모델 성능 비교
- 하이퍼파라미터 튜닝
4.1 GridSearch: LightGBM
4.2 GridSearch: XGBoost
4.3 RandomSearch: LightGBM
4.4 RandomSearch: XGBoost
4.5 최적의 Threshold 찾기
- Submission
이 중에서 핵심적으로 다룰 내용은 3.모델 비교 및 선택 과 4.하이퍼파라미터 튜닝 부분이다.
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix, f1_score, roc_auc_score
from imblearn.over_sampling import SMOTE
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
import os
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")
def get_clf_eval(y_test, pred, pred_proba=None):
"""
이진 분류 모델의 주요 성능 지표를 출력하는 평가 함수
Parameters
----------
y_test : array-like
실제 레이블 값 (정답)
pred : array-like
분류 모델의 예측 결과 (0 또는 1)
pred_proba : array-like, optional
클래스 1에 대한 예측 확률 (ROC-AUC 계산용)
Returns
-------
None
평가 지표를 출력만 하고 반환값은 없음
출력 내용
-------
- 오차 행렬 (Confusion Matrix)
- 정확도 (Accuracy)
- 정밀도 (Precision)
- 재현율 (Recall)
- F1 스코어
- ROC-AUC (클래스 1의 예측 확률 기반)
"""
confusion = confusion_matrix(y_test, pred)
accuracy = accuracy_score(y_test, pred)
precision = precision_score(y_test, pred)
recall = recall_score(y_test, pred)
f1 = f1_score(y_test, pred)
# 예측 확률값이 없는 경우 에러 방지
roc_auc = roc_auc_score(y_test, pred_proba) if pred_proba is not None else None
print('오차 행렬')
print(confusion)
print(f'정확도: {accuracy:.4f}, 정밀도: {precision: .4f}, 재현율: {recall: .4f}, f1스코어: {f1:.4f}')
print(f'ROC-AUC: {roc_auc:.4f}')
from lightgbm import LGBMClassifier, early_stopping, log_evaluation
from xgboost import XGBClassifier
def get_model_train_eval(model, ftr_train=None, ftr_test=None, tgt_train=None, tgt_test=None):
"""
모델 학습 및 평가 수행 함수 (LightGBM, XGBoost는 early stopping 자동 적용)
Parameters
----------
model : classifier model object
학습 및 예측에 사용할 분류 모델 객체
ftr_train : array-like
학습용 feature 데이터
ftr_test : array-like
테스트용 feature 데이터
tgt_train : array-like
학습용 target 값
tgt_test : array-like
테스트용 target 값
Returns
-------
없음 (내부에서 평가 결과를 출력함)
"""
eval_set = [(ftr_train, tgt_train), (ftr_test, tgt_test)]
# LightGBM 또는 XGBoost의 경우 early stopping 적용
if isinstance(model, LGBMClassifier):
model.fit(
ftr_train, tgt_train,
eval_set=eval_set,
eval_metric='auc',
# 조기 종료 로그, 평가 지표 로그 제거
callbacks=[early_stopping(stopping_rounds=100, verbose=False), log_evaluation(period=0)]
)
elif isinstance(model, XGBClassifier):
model.fit(
ftr_train, tgt_train,
eval_set=eval_set,
verbose=False
)
else:
# 일반 모델 학습
model.fit(ftr_train, tgt_train)
# 예측 및 평가
pred = model.predict(ftr_test)
pred_proba = model.predict_proba(ftr_test)[:, 1]
get_clf_eval(tgt_test, pred, pred_proba)
머신러닝 분류 모델들을 하이퍼파라미터 튜닝 없이 default parameters 상태로 학습시킨 후, 각 모델의 전반적인 예측 성능을 비교하고자 진행한다.
# 모델 객체 생성
log_clf = LogisticRegression(max_iter=1000) # 수렴 실패 방지
lgbm_clf = LGBMClassifier(boost_from_average=False, # 불균형 데이터이므로 모델 자체 학습 유도
# force_row_wise=True, # 멀티스레딩 경고 로그 제거
random_state=42,
n_jobs=-1,
verbosity=-1
)
xgb_clf = XGBClassifier(eval_metric='auc',
early_stopping_rounds=10,
use_label_encoder=False,
random_state=42,
n_jobs=-1
)
rf_clf = RandomForestClassifier(random_state=42, n_jobs=-1)
dt_clf = DecisionTreeClassifier(random_state=42)
knn_clf = KNeighborsClassifier()
ada_clf = AdaBoostClassifier(random_state=42)
"""
각 분류 모델별로 model을 변경하여 사용한다.
이 부분은 함수화하여 사용할 수 있다.
"""
# LogisticRegression - 기존 블균형 훈련 데이터
get_model_train_eval(model=log_clf,
ftr_train=X_train,
ftr_test=X_test,
tgt_train=y_train,
tgt_test=y_test)
# LogisticRegression - SMOTE 적용한 오버샘플링 데이터
get_model_train_eval(model = log_clf,
ftr_train=X_train_over,
ftr_test=X_test,
tgt_train=y_train_over,
tgt_test=y_test
)
early stopping 파라미터 반환 함수
def get_fit_params_with_early_stopping(model, X_train, X_test, y_train, y_test):
"""
GridSearchCV/RandomizedSearchCV의 fit() 호출 시 사용할 fit_params 반환
Parameters
----------
model : 학습 대상 모델 객체
X_train, X_test, y_train, y_test : 학습/평가 데이터
Returns
-------
fit_params : dict
모델 학습 시 fit()에 전달할 파라미터
"""
eval_set = [(X_train, y_train), (X_test, y_test)]
if isinstance(model, LGBMClassifier):
return {
"eval_set": eval_set,
"eval_metric": "auc",
"callbacks": [
early_stopping(stopping_rounds=100, verbose=False),
log_evaluation(period=0)
]
}
elif isinstance(model, XGBClassifier):
return {
"eval_set": eval_set,
"verbose": False
}
else:
# 일반 모델은 fit_params 없이 return
return {}
from sklearn.metrics import classification_report, roc_auc_score
def evaluate_best_model(grid_cv, X_test, y_test):
"""
GridSearchCV/RandomizedSearchCV로 튜닝된 최적 모델을 테스트 세트에 대해 평가하는 함수
Parameters
----------
grid_cv : GridSearchCV 또는 RandomizedSearchCV 객체 (fit 완료된 상태)
X_test : 테스트 피처
y_test : 테스트 타겟
Prints
------
- 최적 하이퍼파라미터
- 교차 검증 기반 Best F1 score
- 테스트 ROC-AUC
- 테스트 데이터의 classification_report: precision, recall, f1-score, support
"""
print("Best Hyperparameters:", grid_cv.best_params_)
print("Best F1 Score:", grid_cv.best_score_)
best_model = grid_cv.best_estimator_
pred = best_model.predict(X_test)
pred_proba = best_model.predict_proba(X_test)[:, 1]
print("Test ROC-AUC:", roc_auc_score(y_test, pred_proba))
print(classification_report(y_test, pred))
return best_model
뷸균형 데이터에서 해석 Tip)
| 지표 | 설명 | 예시 해석 |
|---|---|---|
| precision | 사기라고 한 것 중 진짜 사기 | 0.90 → 10%는 잘못 판단 (FP를 줄여야 함) |
| recall | 진짜 사기 중에 찾은 비율 | 0.82 → 18%는 놓침 (FN을 줄여야 함) |
| f1-score | 둘의 균형(조화 평균) | imbalanced data에서 중요 |
| support | 각 클래스(label)의 실제 샘플 수 | 불균형 주의 필요 |
모델 성능 평가는 F1-score, ROC-AUC 등 다양한 분류 성능 지표를 기준으로 수행한다. 최종 선택된 모델은 이후 threshold 조정 및 제출 과정에 활용된다.
from lightgbm import LGBMClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report, roc_auc_score
# 튜닝 대상 파라미터
lgbm_params = {
'n_estimators': [100, 200],
'learning_rate': [0.05, 0.1],
'max_depth': [3, 5, 7],
# 'num_leaves': [31, 64],
}
# 모델 정의
lgbm_clf = LGBMClassifier(boost_from_average=False,
random_state=42,
n_jobs=-1,
verbosity=-1
)
# 조기 종료용 fit_params
fit_params = get_fit_params_with_early_stopping(lgbm_clf, X_train, X_test, y_train, y_test)
# GridSearchCV 정의
grid_cv_lgbm = GridSearchCV(lgbm_clf, lgbm_params, cv=3, scoring='f1', n_jobs=-1, verbose=1)
# 학습 시 추가 fit_params 전달
grid_cv_lgbm.fit(X_train, y_train, **fit_params)
# 최적 모델로 예측 및 평가
best_lgbm_grid = evaluate_best_model(grid_cv_lgbm, X_test, y_test)
from xgboost import XGBClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import classification_report, roc_auc_score
# 튜닝 대상 파라미터
xgb_params = {
'n_estimators': [100, 200],
'learning_rate': [0.05, 0.1],
'max_depth': [3, 5, 7],
'subsample': [0.8, 1.0],
'colsample_bytree': [0.8, 1.0],
}
# 모델 정의
xgb_clf = XGBClassifier(eval_metric='auc',
early_stopping_rounds=10,
use_label_encoder=False,
random_state=42,
n_jobs=-1
)
# 조기 종료용 fit_params
fit_params = get_fit_params_with_early_stopping(xgb_clf, X_train, X_test, y_train, y_test)
# GridSearchCV 정의
grid_cv_xgb = GridSearchCV(xgb_clf, xgb_params, cv=3, scoring='f1', n_jobs=-1, verbose=1)
# 학습 시 추가 fit_params 전달
grid_cv_xgb.fit(X_train, y_train, **fit_params)
# 최적 모델로 예측 및 평가
best_xgb_grid = evaluate_best_model(grid_cv_xgb, X_test, y_test)
from lightgbm import LGBMClassifier
from sklearn.model_selection import RandomizedSearchCV
from sklearn.metrics import classification_report, roc_auc_score
# 튜닝 대상 파라미터
lgbm_param_dist = {
'n_estimators': [100, 200, 300],
'learning_rate': [0.01, 0.05, 0.1],
'max_depth': [3, 5, 7, -1],
'num_leaves': [31, 64, 128],
'subsample': [0.7, 0.8, 1.0],
'colsample_bytree': [0.7, 0.8, 1.0]
}
# 모델 정의
lgbm_clf = LGBMClassifier(boost_from_average=False, random_state=42, n_jobs=-1, verbosity=-1)
# 조기 종료용 fit_params
fit_params = get_fit_params_with_early_stopping(lgbm_clf, X_train, X_test, y_train, y_test)
# RandomizedSearchCV 정의
rand_cv_lgbm = RandomizedSearchCV(
estimator=lgbm_clf,
param_distributions=lgbm_param_dist,
n_iter=20,
scoring='f1',
cv=3,
n_jobs=-1,
verbose=1,
random_state=42
)
# 학습 시 추가 fit_params 전달
rand_cv_lgbm.fit(X_train, y_train, **fit_params)
# 최적 모델로 예측 및 평가
best_lgbm_rand = evaluate_best_model(rand_cv_lgbm, X_test, y_test)
from xgboost import XGBClassifier
from sklearn.model_selection import RandomizedSearchCV
from sklearn.metrics import classification_report, roc_auc_score
# 튜닝 대상 파라미터
xgb_param_dist = {
'n_estimators': [100, 200, 300],
'learning_rate': [0.01, 0.05, 0.1],
'max_depth': [3, 5, 7],
'subsample': [0.7, 0.8, 1.0],
'colsample_bytree': [0.7, 0.8, 1.0],
'gamma': [0, 0.1, 0.3, 1]
}
# 모델 정의
xgb_clf = XGBClassifier(use_label_encoder=False, random_state=42, n_jobs=-1)
# 조기 종료용 fit_params
fit_params = get_fit_params_with_early_stopping(xgb_clf, X_train, X_test, y_train, y_test)
# RandomizedSearchCV 정의
rand_cv_xgb = RandomizedSearchCV(
estimator=xgb_clf,
param_distributions=xgb_param_dist,
n_iter=20,
scoring='f1',
cv=3,
n_jobs=-1,
verbose=1,
random_state=42
)
# 학습 시 추가 fit_params 전달
rand_cv_xgb.fit(X_train, y_train, **fit_params)
# 최적 모델로 예측 및 평가
best_xgb_rand = evaluate_best_model(rand_cv_xgb, X_test, y_test)
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix, f1_score, precision_score, recall_score
# 확률 기반 예측
pred_proba = best_lgbm_rand.predict_proba(X_test)[:, 1] # best 모델 변경 필요
# threshold 조정에 대한 성능 평가
for thresh in [0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]:
y_pred = (pred_proba >= thresh).astype(int)
tp = ((y_test == 1) & (y_pred == 1)).sum()
fp = ((y_test == 0) & (y_pred == 1)).sum()
fn = ((y_test == 1) & (y_pred == 0)).sum()
precision = tp / (tp + fp + 1e-6)
recall = tp / (tp + fn + 1e-6)
f1 = 2 * (precision * recall) / (precision + recall + 1e-6)
print(f"Threshold: {thresh:.1f} | F1: {f1:.4f} | Precision: {precision:.4f} | Recall: {recall:.4f}")
best_threshold = 0.5
y_pred_final = (pred_proba >= best_threshold).astype(int)
cm = confusion_matrix(y_test, y_pred_final)
print(cm)
print("Positive 예측 개수:", y_pred_final.sum())
이후 다양한 조합으로 하이퍼파라미터 조정 실험을 수행하거나, Stack 모델을 적용해 보는 등 예측 성능 향상을 위한 여러 시도를 해 볼 수 있다.
어떤 상황에도 찰떡같이 적용할 수 있는 로직은 아니겠지만, 앞으로 다양한 문제를 해결하면서 고도화해 갈 수 있는 방향을 제시할 수 있을 거라 기대한다.
추후 버전 업데이트가 생기면 수정해 두겠다.