레이블이 불균형한 분포를 가진 데이터 세트를 학습시킬 때 예측 성능의 문제가 발생할 수 있다.
이상 레이블을 가지는 데이터 건수는 매우 적기 때문에 제대로 다양한 유형을 학습하지 못하는 반면에 정상 레이블을 가지는 데이터 건수는 매우 많기 때문에 일방적으로 정상 레이블로 치우친 학습을 수행해 제대로 된 이상 데이터 검출이 어려워지기 쉽다.
지도학습애서 극도로 불균형한 레이블 값 분포로 인한 문제점을 해결하기 위해서는 적절한 학습 데이터를 확보하는 방안이 필요한데, 대표적으로 오버 샘플링(Oversampling)과 언더 샘플링(Undersampling) 방법이 있으며, 오버 샘플링 방식이 예측 성능상 더 유리한 경우가 많아 주로 사용된다.
(1) 언더 샘플링
많은 레이블을 가진 데이터 세트를 적은 레이블을 가진 데이터 세트 수준으로 감소
과도하게 정상 레이블로 학습/예측하는 부작용을 개선할 수 있지만, 너무 많은 정상 레이블 데이터를 감소시키기 때문에 정상 레이블의 경우 오히려 제대로 된 학습을 수행할 수 없다는 단점이 있어 잘 적용하지 않는 방법이다.
(2) 오버 샘플링
적은 레이블을 가진 데이터 세트를 많은 레이블을 가진 데이터 세트 수준으로 증식
동일한 데이터를 단순히 증식하는 방법은 과적합이 되기 때문에 의미가 없으므로 원본 데이터의 피처 값들을 아주 약간만 변경하여 증식한다.
대표적으로 SMOTE(Synthetic Minority Over-sampling Technique) 방법이 있다. SMOTE는 적은 데이터 세트에 있는 개별 데이터들의 K 최근접 이웃(K Nearest Neighbor)을 찾아서 이 데이터와 K개 이웃들의 차이를 일정 값으로 만들어서 기존 데이터와 약간 차이가 나는 새로운 데이터들을 생성하는 방식이다.
# 라이브러리 import pandas as pd import numpy as np import seaborn as sns from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from lightgbm import LGBMClassifier from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score, roc_auc_score import matplotlib.pyplot as plt import warnings warnings.filterwarnings("ignore") %matplotlib inline # 인자로 입력받은 DataFrame을 복사 한 뒤 Time 컬럼만 삭제하고 복사된 DataFrame 반환 함수 def get_preprocessed_df(df=None): df_copy = df.copy() df_copy.drop('Time', axis=1, inplace=True) return df_copy # 사전 데이터 가공 후 학습과 테스트 데이터 세트를 반환하는 함수 def get_train_test_dataset(df=None): # 인자로 입력된 DataFrame의 사전 데이터 가공이 완료된 복사 DataFrame 반환 df_copy = get_preprocessed_df(df) # DataFrame의 맨 마지막 컬럼이 레이블, 나머지는 피처들 X_features = df_copy.iloc[:, :-1] y_target = df_copy.iloc[:, -1] # train_test_split( )으로 학습과 테스트 데이터 Stratified 기반 분할 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 # 평가 함수 def get_clf_eval(y_test, pred=None, pred_proba=None): 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) print('오차 행렬') print(confusion) print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f}, F1: {3:.4f}, AUC:{4:.4f}'.format(accuracy, precision, recall, f1, roc_auc)) # 인자로 사이킷런의 Estimator 객체와 학습/테스트 데이터 세트를 입력 받아서 학습/예측/평가 수행 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)
# 데이터 불러오기 card_df = pd.read_csv('./creditcard.csv') # 데이터 전처리 X_train, X_test, y_train, y_test = get_train_test_dataset(card_df) # 로지스틱 회귀 모델 lr_clf = LogisticRegression() get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test) # LightGBM 모델 lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False) get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)
- 레이블 값이 극도로 불균형한 분포를 이루는 경우
boost_from_average=True
설정은 재현률 및 ROC-AUC 성능을 매우 크게 저하시키므로boost_from_average=False
로 파라미터를 설정해야 한다.
로지스틱 회귀는 선형 모델로, 대부분의 선형 모델은 중요 피처들의 값이 정규 분포 형태를 유지하는 것을 선호
왜곡된 분포도를 가지는 데이터를 재가공한 뒤에 모델을 다시 테스트
# Amount 피처의 분포도 plt.figure(figsize=(8, 4)) plt.xticks(range(0, 30000, 1000), rotation=60) sns.distplot(card_df['Amount'])
- 카드 사용 금액이 1000불 이하인 데이터가 대부분이며, 27000불까지 드물지만 많은 금액을 사용한 경우가 발생하면서 꼬리가 긴 형태의 분포 곡선을 가지고 있다.
- 따라서 Amount를 정규 분포 형태로 변환
# StandardScaler를 이용하여 정규분포 형태로 Amount 피처값을 변환하는 로직으로 수정 def get_preprocessed_df(df=None): df_copy = df.copy() scaler = StandardScaler() amount_n = scaler.fit_transform(df_copy['Amount'].values.reshape(-1, 1)) # 변환된 Amount를 Amount_Scaled로 피처명 변경후 DataFrame맨 앞 컬럼으로 입력 df_copy.insert(0, 'Amount_Scaled', amount_n) # 기존 Time, Amount 피처 삭제 df_copy.drop(['Time','Amount'], axis=1, inplace=True) return df_copy # Amount를 정규분포 형태로 변환 후 로지스틱 회귀 및 LightGBM 수행 X_train, X_test, y_train, y_test = get_train_test_dataset(card_df) print('### 로지스틱 회귀 예측 성능 ###') lr_clf = LogisticRegression() get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test) print('### LightGBM 예측 성능 ###') lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False) get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)
# Amount 피처값 로그 변환 함수 def get_preprocessed_df(df=None): df_copy = df.copy() # 넘파이의 log1p( )를 이용하여 Amount를 로그 변환 amount_n = np.log1p(df_copy['Amount']) df_copy.insert(0, 'Amount_Scaled', amount_n) df_copy.drop(['Time','Amount'], axis=1, inplace=True) return df_copy # Amount 로그 변환 후 로지스틱 회귀 및 LightGBM 수행 X_train, X_test, y_train, y_test = get_train_test_dataset(card_df) print('### 로지스틱 회귀 예측 성능 ###') get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test) print('### LightGBM 예측 성능 ###') get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)
이상치 데이터(Outlier): 전체 데이터의 패턴에서 벗어난 이상 값을 가진 데이터
이상치로 인해 머신러닝 모델의 성능에 영향을 받는 경우가 발생하기 쉽다.
매우 많은 피처가 있을 경우 이들 중 결정값(레이블)과 가장 상관성이 높은 피처들을 위주로 이상치를 검출하는 것이 좋다.
IQR(Inter Quantile Range)
사분위
전체 데이터를 오름차순 정렬한 뒤, 이를 1/4(25%)씩으로 구간을 분할한다.
순서대로 Q1(25%), Q2(50%), Q3(75%), Q4(100%)로 나뉜다.IQR
25% 구간인 Q1 ~ 75% 구간인 Q3의 범위이상치 데이터 검출 방식
보통 IQR에 1.5를 곱해서 생성된 범위를 이용해 최댓값과 최솟값을 결정한 뒤 최댓값을 초과하거나 최솟값을 미달하는 데이터를 이상치로 간주한다.
1.5가 아닌 다른 값을 적용할 수도 있으며, 보통은 1.5로 적용한다.
- 최댓값:
Q3 + IQR * 1.5
- 최솟값:
Q1 - IQR * 1.5
박스 플롯
IQR 방식을 시각화한 도표로 사분위의 편차와 IQR, 이상치를 나타낸다.
# 피처별 상관계수 plt.figure(figsize=(9, 9)) corr = card_df.corr() sns.heatmap(corr, cmap='RdBu') # 양의 상관관계가 높을수록 진한 파란색, # 음의 상관관계가 높을수록 진한 빨간색
- 결정 레이블인 Class 피처와 음의 상관관계가 가장 높은 피처는 V14, V17
# 이상치 검출 함수 def get_outlier(df=None, column=None, weight=1.5): # fraud에 해당하는 column 데이터만 추출, 1/4분위와 3/4분위 지점을 np.percentile로 구함 fraud = df[df['Class']==1][column] quantile_25 = np.percentile(fraud.values, 25) quantile_75 = np.percentile(fraud.values, 75) # IQR을 구하고, IQR에 1.5를 곱하여 최대값과 최소값 지점 구함 iqr = quantile_75 - quantile_25 iqr_weight = iqr * weight lowest_val = quantile_25 - iqr_weight highest_val = quantile_75 + iqr_weight # 최대값보다 크거나, 최소값보다 작은 값을 아웃라이어로 설정하고 DataFrame index 반환 outlier_index = fraud[(fraud < lowest_val) | (fraud > highest_val)].index return outlier_index # get_processed_df()를 로그 변환 후 V14 피처의 이상치 데이터를 삭제하는 로직으로 변경 def get_preprocessed_df(df=None): df_copy = df.copy() amount_n = np.log1p(df_copy['Amount']) df_copy.insert(0, 'Amount_Scaled', amount_n) df_copy.drop(['Time','Amount'], axis=1, inplace=True) # 이상치 데이터 삭제하는 로직 추가 outlier_index = get_outlier(df=df_copy, column='V14', weight=1.5) df_copy.drop(outlier_index, axis=0, inplace=True) return df_copy X_train, X_test, y_train, y_test = get_train_test_dataset(card_df) print('### 로지스틱 회귀 예측 성능 ###') get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test) print('### LightGBM 예측 성능 ###') get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)
SMOTE를 적용할 때는 반드시 학습 데이터 세트만 오버 샘플링을 해야 한다.
검증 데이터 세트나 테스트 데이터 세트를 오버 샘플링할 경우 결국은 원본 데이터 세트가 아닌 데이터 세트에서 검증 또는 테스트를 수행하기 때문에 올바른 검증/테스트가 될 수 없다.
좋은 SMOTE 패키지일수록 재현율 증가율은 높이고 정밀도 감소율은 낮출 수 있도록 효과적으로 데이터를 증식한다.
from imblearn.over_sampling import SMOTE smote = SMOTE(random_state=0) X_train_over, y_train_over = smote.fit_resample(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() get_model_train_eval(lr_clf, ftr_train=X_train_over, ftr_test=X_test, tgt_train=y_train_over, tgt_test=y_test) - 재현율이 92.47%로 크게 증가하지만, 반대로 정밀도가 5.4%로 급격하게 저하되었다. - 오버 샘플링으로 인해 실제 원본 데이터의 유형보다 너무나 많은 Class=1 데이터를 학습하면서 실제 테스트 데이터 세트에서 예측을 지나치게 Class=1로 적용해 정밀도가 급격히 떨어진 것이다. ```python import matplotlib.pyplot as plt import matplotlib.ticker as ticker from sklearn.metrics import precision_recall_curve %matplotlib inline def precision_recall_curve_plot(y_test , pred_proba_c1): # threshold ndarray와 이 threshold에 따른 정밀도, 재현율 ndarray 추출 precisions, recalls, thresholds = precision_recall_curve(y_test, pred_proba_c1) # X축을 threshold값으로, Y축은 정밀도, 재현율 값으로 각각 Plot 수행. 정밀도는 점선으로 표시 plt.figure(figsize=(8,6)) threshold_boundary = thresholds.shape[0] plt.plot(thresholds, precisions[0:threshold_boundary], linestyle='--', label='precision') plt.plot(thresholds, recalls[0:threshold_boundary],label='recall') # threshold 값 X 축의 Scale을 0.1 단위로 변경 start, end = plt.xlim() plt.xticks(np.round(np.arange(start, end, 0.1),2)) # x축, y축 label과 legend, 그리고 grid 설정 plt.xlabel('Threshold value'); plt.ylabel('Precision and Recall value') plt.legend(); plt.grid() plt.show() precision_recall_curve_plot(y_test, lr_clf.predict_proba(X_test)[:, 1])
- 분류 결정 임곗값을 조정하더라도 임계값의 민감도가 너무 심해 올바른 재현율/정밀도 성능을 얻을 수 없으므로 로지스틱 회귀 모델의 경우 SMOTE 적용 후 올바른 예측 모델이 생성되지 못했다.
# LightGBM 모델 lgbm_clf = LGBMClassifier(n_estimators=1000, num_leaves=64, n_jobs=-1, boost_from_average=False) get_model_train_eval(lgbm_clf, ftr_train=X_train_over, ftr_test=X_test, tgt_train=y_train_over, tgt_test=y_test)
- 재현율은 높아졌지만 LightGBM 역시 로지스틱 회귀 모델만큼 급격하게는 아니더라도 정밀도가 낮아졌다.
- SMOTE를 적용하면 재현율은 높아지나, 정밀도는 낮아지는 것이 일반적이다.