본문 바로가기

Lecture ML

머신러닝 강좌 #22] SMOTE, LightGBM을 이용한 예측 분류 실습 - 캐글 신용카드 사기 검출

반응형

신용카드 데이터 세트를 이용한 검출 분류 실습을 해보겠습니다.  해당 데이터 세트의 레이블인 Class속성은 매우 불균형한 분포를 가지고 있습니다. Class는 0과 1로 분류되는데 0이 사기가 아닌 정상적인 신용카드 트랜잭션 데이터, 1은 신용카드 사기 트랜잭션을 의미합니다.

 

전체 데이터의 약 0.172%만이 레이블 값이 1, 즉 사기 트랜잭션입니다. 일반적으로 사기 검출이나 이상 검출과 같은 데이터 세트는 이처럼 레이블 값이 극도로 불균형한 분포를 가지기 쉽습니다. 왜냐하면 사기와 같은 이상 현상은 전체 데이터에서 차지하는 비중이 매우 적을 수밖에 없기 때문입니다.

 

언더 샘플링과 오버 샘플링의 이해

레이블이 불균형한 분포를 가진 데이터 세트를 학습시킬 때 예측 성능의 문제가 발생할 수 있는데, 이는 이상 레이블을 가지는 데이터 건수가 정상 레이블을 가진 데이터 건수에 비해 너무 적기 때문에 발생합니다. 즉 이상 레이블을 가지는 데이터 건수는 매우 적기 때문에 제대로 다양한 유형을 학습하지 못하는 반면에 정상 레이블을 가지는 데이터 건수는 매우 많기 때문에 일방적으로 정상 레이블로 치우친 학습을 수행해 제대로 된 이상 데이터 검출이 어려워지기 쉽습니다. 지도 학습에서 극도로 불균형한 레이블 값 분포로 인한 문제점을 해결하기 위해서는 적절한 학습 데이터를 확보한 방안이 필요한데, 대표적으로 오버샘플링(Oversampling)과 언더 샘플링(Undersampling) 방법이 있으며, 오버샘플링 방식이 예측 성능상 더 유리한 경우가 많아 주로 사용됩니다.

 

  • 언더 샘플링은 많은 데이터 세트를 적은 데이터 세트 수준으로 감소시키는 방식입니다. 즉 정상 레이블을 가진 데이터가 10,000건 이상 레이블을 가진 데이터가 100건이 있으면 정상 레이블 데이터를 100건으로 줄여 버리는 방식입니다. 이렇게 정상 레이블 데이터를 이상 레이블 데이터 수준으로 줄여 버린 상태에서 학습을 수행하면 과도하게 정상 레이블로 학습/예측하는 부작용을 개선할 수 있지만, 너무 많ㅇㄴ 정상 레이블 데이터를 감소시키기 때문에 정상 레이블의 경우 오히려 제대로 된 학습을 수행할 수 없다는 단점이 있어 잘 적용하지 않는 방법입니다.
  • 오버 샘플링은 이상 데이터와 같이 적은 데이터 세트를 증식하여 학습을 위한 충분한 데이터를 확보하는 방법입니다. 동일한 데이터를 단순히 증식하는 방법은 과적합(Overfitting)이 되기 때문에 의미가 없으므로 원본 데이터의 피처 값들을 아주 약간만 변경하여 증식합니다. 대표적으로 SMOTE방법이 있습니다. SMOTE는 적은 데이터 세트에 있는 개별 데이터들의 K최근접 이윳을 찾아서 이 데이터와 K개 이웃들의 차이를 일정 값으로 만들어서 기존 데이터와 약간 차이가 나는 새로운 데이터들을 생성하는 방식입니다.

 

SMOTE를 구현한 대표적인 파이썬 패키지는 imbalanced-learn입니다. 

 

데이터전처리 

from sklearn.model_selection import train_test_split

def get_preprocessed(df=None):
    df_copy = df.copy()
    df_copy.drop('Time', axis=1, inplace=True)

    return df_copy

def get_train_test_Dataset(df=None):
    df_copy = get_preprocessed(df)
    X_features = df_copy.iloc[:,:-1]
    y_target = df_copy.iloc[:,-1]
    X_train, X_test, y_train, y_test = train_test_split(X_features, y_target, test_size=0.3, random_state=0, stratify=y_target)

    return X_train, X_test, y_train, y_test

X_train, X_test, y_train, y_test = get_train_test_Dataset(card_df)
print(y_train.value_counts()/y_train.shape[0]*100)
print(y_test.value_counts()/y_test.shape[0]*100)

 

학습 데이터 레이블의 경우 1 값이 약 0.172%, 테스트 이터 레이블의 경우 1 값이 약 0.173%로 큰 차이가 없이 잘 분할됐습니다. 이제 모델을 만들어 보겠습니다.

0    99.827451
1     0.172549
Name: Class, dtype: float64
0    99.826785
1     0.173215

 

이제 모델을 만들어 보겠습니다. 로지스틱 회귀와 LightGBM기반의 모델이 데이터 가공을 수행하면서 예측 성능이 어떻게 변하는지 살펴볼 것입니다. 먼저 로지스틱 회귀를 이용해 신요 카드 사기 여부를 예측해 보겠습니다.

from sklearn.linear_model import LogisticRegression

lr_clf = LogisticRegression()
lr_clf.fit(X_train, y_train)
lr_pred = lr_clf.predict(X_test)
lr_pred_proba = lr_clf.predict_proba(X_test)[:,1]

CMStat.get_clf_eval(y_test, lr_pred, lr_pred_proba)

 

재현율은 0.619입니다.

정확도: 0.9992
정밀도: 0.8333
재현율: 0.6419
F1: 0.7252

 

이번에는 LightGBM을 이용한 모델을 만들어 보겠습니다. 그에 앞서, 앞으로 수행할 예제 코드에서 반복적으로 모델을 변경해 학습/예측/평가할 것이므로 이를 위한 별도의 함수를 생성하겠습니다. 

def get_model_train_eval(model, ftr_train=None, ftr_test=None, tgt_train=None, tgt_test=None):
    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)


from lightgbm import LGBMClassifier
lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False)
CMStat.get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

 

 결과를 확인하면 정확도, 정밀도, 재현율 및 F1값 모두 상승했음을 알 수 있습니다.

정확도: 0.9995
정밀도: 0.9573
재현율: 0.7568
F1: 0.8453

 

데이터 분포도 변환 후 모델 학습/예측 평가

이번에는 왜고괸 분포도를 가지는 데이터를 재가공한 뒤에 모델을 다시 테스트해 보겠습니다. 먼저 각 feature의 분포도를 살펴봅니다. 로지스틱 회귀는 선형 모델입니다. 대부분의 선형 모델은 중요 피처들의 값이 정규 분포 형태를 유지하는 것을 선호합니다. Amount피처는 신용 카드 사용 금액으로 정상/사기 트랜잭션을 결정하는 매우 중요한 속성일 가능성이 높습니다.

 

import seaborn as sns
plt.figure(figsize=(8,4))
plt.xticks(range(0, 30000, 1000), rotation=60)
sns.distplot(card_df['Amount'])
plt.show()

 

Amount, 즉 카드 사용금액이 1000불 이하인 데이터가 대부분이며, 27,000불까지 드물지만 많은 금액을 사용한 경우가 발생하면서 꼬리가 긴 형태의 분포 곡선을 가지고 있습니다. Amount를 표준 정규 분포 형태로 변환한 뒤에 로지스틱 회귀의 예측성능을 측정해 보겠습니다.

 

이를 위해 앞에서 만든 get_processed_df() 함수를 다음과 같이 사이킷런의 StandardScaler클래스를 이용해 Amount피처를 정규 분포 형태로 변환하는 코드로 변경합니다.

 

from sklearn.preprocessing import StandardScaler
#사이킷런의 StandardScaler를 이용해 정규 분포 형태로 Amount피처값 변환하는 로직으로 수정.
def get_preprocessed_df(df=None):
    df_copy = df.copy()
    scaler = StandardScaler()
    amount_n = scaler.fit_transform(df_copy['Amount'].values.reshpe(-1,1))
    #변환된 Amount를 Amount_Scaled로 피처명 변경 후 DataFrame맨 앞 칼럼으로 입력
    df_copy.insert(0, 'Amount_Scled', amount_n)
    
    #기존 Time, Amount 피처 삭제
    df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
    return df_copy    


print('-LogisticRegression')
lr_clf = LogisticRegression()
CMStat.get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

print('-LGBMClassifier')
lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1)
CMStat.get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

 

결과를 보면 LogisticRegression와 LGBMClassifier해도 성능이 크게 개선되지는 않았습니다.

-LogisticRegression
오차행렬:
 [[85276    19]
 [   53    95]]

정확도: 0.9992
정밀도: 0.8333
재현율: 0.6419
F1: 0.7252

-LGBMClassifier
오차행렬:
 [[85067   228]
 [   73    75]]

정확도: 0.9965
정밀도: 0.2475
재현율: 0.5068
F1: 0.3326

 

이번에는 StandardScaler가 아니라 로그 변환을 수행해 보겠습니다. 로그 변환은 데이터 분포도가 심하게 왜곡되어 있을 경우 적용하는 중요 기법 중에 하나입니다. 원래 값을 log값으로 변환해 원래 큰 값을 상대적으로 작은 값으로 변환하기 때문에 데이터 분포도의 왜곡을 상당 수준 개선해 줍니다. 로그 변환은 넘파이의 log1p()함수를 이용해 간단히 변환이 가능합니다.

 

def get_preprocessed_df(df=None):
    df_copy = df.copy()

    #넘파이의 log1p()를 이용해 Amount를 로그 변환
    amount_n = np.log1p(df_copy['Amount'])
    df_copy.insert(0, 'Amount_Scled', amount_n)

    #기존 Time, Amount 피처 삭제
    df_copy.drop(['Time', 'Amount'], axis=1, inplace=True)
    return df_copy

X_train, X_test, y_train, y_test = get_train_test_Dataset(card_df)
print('-LogisticRegression')
CMStat.get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)
print('-LGBMClassifier')
CMStat.get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

 

정밀도가 어느정도 향상되었음을 알 수 있습니다.

-LogisticRegression
오차행렬:
 [[85276    19]
 [   53    95]]

정확도: 0.9992
정밀도: 0.8333
재현율: 0.6419
F1: 0.7252

-LGBMClassifier
오차행렬:
 [[85210    85]
 [   82    66]]

정확도: 0.9980
정밀도: 0.4371
재현율: 0.4459
F1: 0.4415

 

 

SMOTE 오버 샘플링 적용 후 모델 학습/예측/평가

이번에는 SMOTE기법으로 오버 샘플링을 적용한 뒤 로지스틱 회귀와 LightGBM모델의 예측 성능을 평가해 보겠습니다. 먼저 SMOTE는 앞에서 설치한 imbalanced-learn패키지의 SMOTE클래스를 이용해 간단하게 구현이 가능합니다. 

 

SMOTE를 적용할 때는 반드시 학습 데이터 세트만 오버 샘플링을 해야 합니다. 검증 데이터 세트나 테스트 세트를 오버 샘플링할 경우 결국은 원본 데이터 세트가 아닌 데이터 세트에서 검증 또는 테스트를 수행하기 때문에 올바른 검증/테스트가 될 수 없습니다.

from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=0)
X_train_over, y_train_over = smote.fit_sample(X_train, y_train)
print('SMOTE 적용 전 학습용 피처/레이블 데이터 세트:', X_train.shape, y_train.shape)
print('SMOTE 적용 후 학습용 피처/레이블 데이터 세트:', X_train_over.shape, y_train_over.shape)
print('SMOTE 적용 후 레이블 값 분포: \n', pd.Series(y_train_over).value_counts())

lr_clf = LogisticRegression()
CMStat.get_model_train_eval(lr_clf, ftr_train=X_train_over, ftr_test=X_test, tgt_train=y_train_over, tgt_test=y_test)

 

아래 결과를 보면 2배에 가까운 데이터 증식이 있습니다. SMOTE적용 후 레이블 값이 0과 1의 분포가 동일하게 생성되었습니다. 정확도는 올라가나 정밀도가 급감하는데 이런 데이터를 현실 업무에 적용할 수는 없습니다.  이는 로지스틱 회귀 모델이 오버 샘플링으로 인해 실제 원본 데이터의 유형보다 너무나 많은 Class=1 데이터를 학습하면서 실제 테스트 데이터 세트에서 예측을 지나치게 Class=1로 적용해 정밀도가 급격히 떨어지게 된 것입니다. 

SMOTE 적용 전 학습용 피처/레이블 데이터 세트: (199364, 29) (199364,)
SMOTE 적용 후 학습용 피처/레이블 데이터 세트: (398040, 29) (398040,)
SMOTE 적용 후 레이블 값 분포: 
 1    199020
0    199020
Name: Class, dtype: int64
오차행렬:
 [[83995  1300]
 [   16   132]]

정확도: 0.9846
정밀도: 0.0922
재현율: 0.8919
F1: 0.1671

Process finished with exit code 0

 

 

분류 결정 임곗값에 따른 정밀도와 재현율 곡선을 통해 SMOTE로 학습된 로지스틱 회귀 모델에 어떠한 문제가 발생하고 있는지 시각적으로 확인해 보겠습니다. 임계값이 0.99 이하에서는 재현율이 매우 좋고 정밀도가 극단적으로 낮다가 0.99 이상에서는 반대로 재현율이 대폭 떨어지고 정밀도가 높아집니다. 분류 결정 임계값을 조정하더라도 임계값의 민감도가 너무 심해 올바른 재현율/정밀도 성능을 얻을 수 없으므로 로지스틱 회귀 모델의 경우 SMOTE적용 후 올바른 예측 모델이 생성되지 못했습니다.

CMPlot.precision_recall_curve_plot(y_test, lr_clf.predict_proba(X_test)[:,1])

 

이번에는 LightGBM모델을 SMOTE로 오버 샘플링된 데이터 세트로 학습/예측/평가를 수행하겠습니다.

lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1)
CMStat.get_model_train_eval(lr_clf, ftr_train=X_train_over, ftr_test=X_test, tgt_train=y_train_over, tgt_test=y_test)

 

SMOTE를 적용하면 재현율은 높아지나, 정밀도는 낮아지는 것이 일반적입니다. 좋은 SMOTE패키지일수록 재현율 증가율은 높이고 정밀도 감소율은 낮출 수 있도록 효과적으로 데이터를 증식합니다.

정확도: 0.9995
정밀도: 0.9219
재현율: 0.7973
F1: 0.8551
반응형