[파이썬 머신러닝 완벽 가이드] 4장. 분류 - 2
# 22.01.18 머신러닝 스터디 4장. 분류 - 2
< 파이썬 머신러닝 완벽가이드 >¶
04. 분류 2 ( p222 ~ p289)¶
5. GBM ( Gradient Boosting Machine )¶
- 부스팅 : 여러개의 약한 학습기를 순차적으로 학습, 예측하면서 잘못 예측한 데이터에 가중치를 부여, 오류를 개선해 나가면서 학습하는 방식
대표적인 부스팅 방식은 AdaBoost, Gradient Boost
AdaBoost
< 내용 참고 >
https://youtu.be/LsK-xG1cLYA
https://bkshin.tistory.com/entry/%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-14-AdaBoost
1개의 노드와 2개의 리프를 가진 트리를 stump라고 한다.
AdaBoost는 여러개의 stump, 즉 여러개의 약한 학습기로 구성되어 있다. 또한 학습기들은 각자 가지는 가중치가 다르다.
따라서 stump가 여러개 나열되어 있을 때 첫번째 stump부터 학습되기 시작하는데, 만약 첫번째 stump에서 error가 발생했다면 해당 값을 다음 stump로 넘겨줄 때 가중치를 높여서 넘겨준다.
다음 stump에서의 샘플은 기존에 있던 데이터의 가중치에 따라 random으로 갱신되어 stump에 입력된다. ( weight가 높아지면 그만큼 뽑힐 확률이 높아짐 !! )
AdaBoost 훈련 과정은 모델의 예측 능력을 향상시킬 것으로 생각되는 특성들만 선택하고, 이는 차원수를 줄이는 동시에 필요없는 특성들을 고려하지 않음으로써 잠재적으로 수행 시간을 개선시킨다.
Gradient Boost
< 내용 참고 > https://youtu.be/3CC4N4z3GJc https://bkshin.tistory.com/entry/%EB%A8%B8%EC%8B%A0%EB%9F%AC%EB%8B%9D-15-Gradient-Boost?category=1057680
- 가중치 업데이트 : 경사하강법 ( 회귀 파트에서 자세히 )
- 하나의 leaf에서부터 시작한다. ( 타겟값에 대한 초기 추정값 )
- 이전 tree의 error가 다음 tree에 영향을 준다.
- 일반적으로 leaf의 개수는 8 ~ 32개
학습률이 어떻게 작용하는지
Residual을 예측할 것 !!
평균치를 첫번째 leaf로 생각할 경우 두번째로 구한 트리에서 기존 트리값을 더한다.
-> 새롭게 나온 결과에서 다시 Residual을 구함
=> 더이상 residual이 작아지지 않을 때 or 입력한 iteration 횟수까지 반복!
# 중복된 피처명 수정
def get_new_feature_name_df(old_feature_name_df):
feature_dup_df = pd.DataFrame(data=old_feature_name_df.groupby('column_name').cumcount(), columns=['dup_cnt'])
feature_dup_df = feature_dup_df.reset_index()
new_feature_name_df = pd.merge(old_feature_name_df.reset_index(), feature_dup_df, how = 'outer')
new_feature_name_df['column_name'] = new_feature_name_df[['column_name', 'dup_cnt']].apply(lambda x: x[0]+'_'+str(x[1])
if x[1] > 0 else x[0], axis = 1)
new_feature_name_df = new_feature_name_df.drop(['index'], axis = 1)
return new_feature_name_df
import pandas as pd
pd.read_csv('./UCI_HAR_Dataset/features.txt', sep ='\s+', header = None, names=['column_index', 'column_name'])
column_index | column_name | |
---|---|---|
0 | 1 | tBodyAcc-mean()-X |
1 | 2 | tBodyAcc-mean()-Y |
2 | 3 | tBodyAcc-mean()-Z |
3 | 4 | tBodyAcc-std()-X |
4 | 5 | tBodyAcc-std()-Y |
... | ... | ... |
556 | 557 | angle(tBodyGyroMean,gravityMean) |
557 | 558 | angle(tBodyGyroJerkMean,gravityMean) |
558 | 559 | angle(X,gravityMean) |
559 | 560 | angle(Y,gravityMean) |
560 | 561 | angle(Z,gravityMean) |
561 rows × 2 columns
import pandas as pd
def get_human_dataset( ):
# 공백으로 분리 -> sep인자를 \s+
feature_name_df = pd.read_csv('./UCI_HAR_Dataset/features.txt', sep ='\s+', header = None, names=['column_index', 'column_name'])
# 중복된 피처명을 수정
new_feature_name_df = get_new_feature_name_df(feature_name_df)
# DataFrame에 피처명을 column으로 부여하기 위해
feature_name = new_feature_name_df.iloc[:, 1].values.tolist()
# 학습 핓 데이터 세트, 테스트 피처 데이터
X_train = pd.read_csv('./UCI_HAR_Dataset/train/X_train.txt', sep ='\s+', names=feature_name)
X_test = pd.read_csv('./UCI_HAR_Dataset/test/X_test.txt', sep ='\s+', names=feature_name)
# 학습 레이블, 테스트 레이블
y_train = pd.read_csv('./UCI_HAR_Dataset/train/y_train.txt', sep ='\s+', header = None, names=['action'])
y_test = pd.read_csv('./UCI_HAR_Dataset/test/y_test.txt', sep ='\s+', header = None, names=['action'])
return X_train, X_test, y_train, y_test
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score
import time
import warnings
warnings.filterwarnings('ignore')
X_train, X_test, y_train, y_test = get_human_dataset()
# GBM 수행 시간 측정을 위해 time사용
start_time = time.time()
gb_clf = GradientBoostingClassifier(random_state=0)
gb_clf.fit(X_train, y_train)
gb_pred = gb_clf.predict(X_test)
gb_accuracy = accuracy_score(y_test, gb_pred)
print('GBM 정확도: {0:.4f}'.format(gb_accuracy))
print('GBM 수행시간: {0:.1f} 초'.format(time.time() - start_time))
GBM 정확도: 0.9389 GBM 수행시간: 573.2 초
GBM의 하이퍼 파라미터¶
- loss : 경사 하강법에서 사용할 비용 함수 ( default = 'deviance' )
- learning_rate : 학습을 진행할 때마다 적용하는 학습률 ( default = 0.1 )
- n_estimator : weak learner의 개수 ( default = 100 )
- subsample : weak learner가 학습에 사용하는 데이터의 샘플링 비율 ( default = 1 )
Grid Search 예제는 시간이 오래걸리는 관계로 Pass~
6. XGBoost¶
- 장점 : 뛰어난 예측 성능, GBM 대비 빠른 수행 시간, 과적합 규제, 나무 가지치기, 내장된 교차 검증, 결손값 자체 처리
XGBoost 의 하이퍼 파라미터¶
- 일반 파라미터
- booster : gbtree ( 트리 기반 ), gblinear ( 선형 모델 ) ( default = gbtree )
- silent : 출력메세지를 나타내고 싶지 않을 때 1로 설정 ( default = 0 )
- nthread : CPU의 실행 스레드 개수를 조정 ( default = 전부 사용 )
- 부스터 파라미터
- learning rate : 학습률, 일반적으로 0.01 ~ 0.2 사이 값을 선호 ( default = 0.1 )
- n_estimator
- min_child_weight : 트리에서 추가적으로 가지를 나눌지를 결정하기 위해 필요한 데이터들의 weight 총합, 클수록 분할 자제 ( 과적합 조절, default = 1)
- gamma : 트리의 리프 노드를 추가적으로 나눌지를 결정할 최소 손실 감소 값. 해당 값보다 큰 손실이 감소돼야 리프 노드를 분리 ( 클수록 과적합 감소, default = 0 )
- max_depth : 트리의 최대 깊이 ( default = 6 )
- subsample : 데이터를 샘플링하는 비율 ( default = 1 )
- colsample_bytree : 트리 생성에 필요한 피처를 임의로 샘플링하는데 사용 ( max_features와 유사, default = 1 )
- reg_lambda : L2 규제 적용 값, 클수록 과적합 감소 효과 ( default = 1 )
- reg_alpha : L1 규제 적용 값, 클수록 과적합 감소 효과 ( default = 0 )
- scale_pos_weight : 비대칭한 클래스로 구성된 데이터 세트의 균형을 유지하기 위한 파라미터 ( default = 1 )
- 학습 태스크 파라미터
- objective : 최솟값을 가져야할 손실 함수를 정의 ( 이진 분류인지 다중 분류인지에 따라 달라짐 )
( binary:logistic, multi:softmax, multi:softprob 등등... ) - eval_metric : 검증에 사용되는 함수를 정의 ( 회귀 default = 'rmse', 분류 default = 'error' )
( rmse, mae, logloss, error, merror, mlogloss, auc )
- objective : 최솟값을 가져야할 손실 함수를 정의 ( 이진 분류인지 다중 분류인지에 따라 달라짐 )
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
dataset = load_breast_cancer()
X_features = dataset.data
y_label = dataset.target
cancer_df = pd.DataFrame(data = X_features, columns = dataset.feature_names)
cancer_df['target'] = y_label
cancer_df.head(3)
mean radius | mean texture | mean perimeter | mean area | mean smoothness | mean compactness | mean concavity | mean concave points | mean symmetry | mean fractal dimension | ... | worst texture | worst perimeter | worst area | worst smoothness | worst compactness | worst concavity | worst concave points | worst symmetry | worst fractal dimension | target | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 17.99 | 10.38 | 122.8 | 1001.0 | 0.11840 | 0.27760 | 0.3001 | 0.14710 | 0.2419 | 0.07871 | ... | 17.33 | 184.6 | 2019.0 | 0.1622 | 0.6656 | 0.7119 | 0.2654 | 0.4601 | 0.11890 | 0 |
1 | 20.57 | 17.77 | 132.9 | 1326.0 | 0.08474 | 0.07864 | 0.0869 | 0.07017 | 0.1812 | 0.05667 | ... | 23.41 | 158.8 | 1956.0 | 0.1238 | 0.1866 | 0.2416 | 0.1860 | 0.2750 | 0.08902 | 0 |
2 | 19.69 | 21.25 | 130.0 | 1203.0 | 0.10960 | 0.15990 | 0.1974 | 0.12790 | 0.2069 | 0.05999 | ... | 25.53 | 152.5 | 1709.0 | 0.1444 | 0.4245 | 0.4504 | 0.2430 | 0.3613 | 0.08758 | 0 |
3 rows × 31 columns
X_train, X_test, y_train, y_test = train_test_split(X_features, y_label, test_size = 0.2, random_state = 156)
from sklearn.metrics import accuracy_score, precision_score, recall_score, roc_auc_score
from sklearn.metrics import f1_score, confusion_matrix
# 평가지표 출력하는 함수
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))
교재 그대로 실행시 오류가 발생 ( 혼자 train, test 잘못 설정해서 생긴 오류 ) -> average 구하는 방식을 추가
( 아무튼 알아두면 좋다 )
< 참고 자료 > https://junklee.tistory.com/116
클래스 불균형(Imbalance) 문제가 있는 데이터셋에서는 Micro-average가 조금 더 효과적인 평가지표가 된다
from xgboost import XGBClassifier
xgb_wrapper = XGBClassifier(n_estimator = 400, learning_rate = 0.1, max_depth = 3)
xgb_wrapper.fit(X_train, y_train)
w_preds = xgb_wrapper.predict(X_test)
w_pred_proba = xgb_wrapper.predict_proba(X_test)[:, 1]
[23:34:59] WARNING: D:\bld\xgboost-split_1637426510059\work\src\learner.cc:576: Parameters: { "n_estimator" } might not be used. This could be a false alarm, with some parameters getting used by language bindings but then being mistakenly passed down to XGBoost core, or some parameter actually being used but getting flagged wrongly here. Please open an issue if you find any such cases. [23:34:59] WARNING: D:\bld\xgboost-split_1637426510059\work\src\learner.cc:1115: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'binary:logistic' was changed from 'error' to 'logloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
get_clf_eval(y_test, w_preds, w_pred_proba)
오차행렬 [[34 3] [ 2 75]] 정확도: 0.9561, 정밀도: 0.9615, 재현율: 0.9740, F1: 0.9677, AUC:0.9947
XGBoost의 조기중단
n_estimator를 400으로 설정하고 조기중단 값을 100으로 설정 시 100번동안 학습 오류가 감소하지 않으면 종료한다!
그 이후로는 가장 작은 오류가 나왔을 때를 기준으로 100번동안 오류가 더 작아지지 않으면 중단한다 !!
- early_stopping_rounds : 조기중단을 위한 반복 횟수 정의
- eval_metric : 조기중단을 위한 평가지표
- eval_set : 검증 세트를 지정하는 것으로, 학습은 학습 데이터로 하되 예측 오류값 평가는 eval_set로 지정된 검증 세트로 평가하는 방식
loss값을 수치적으로 확인하기 위해, 전체적인 데이터를 확인하고 싶은 도구
from xgboost import XGBClassifier
xgb_wrapper = XGBClassifier(n_estimator = 400, learning_rate = 0.1, max_depth = 3)
# 원칙적으로는 test데이터를 넣지 않고 train데이터를 나눈 validation데이터를 넣는다 !! 여기서는 데이터가 적기 때문에 이렇게 한 것!!
evals = [(X_test, y_test)]
xgb_wrapper.fit(X_train, y_train, early_stopping_rounds = 100, eval_metric = "logloss", eval_set=evals, verbose = True)
ws100_preds = xgb_wrapper.predict(X_test)
ws100_pred_proba = xgb_wrapper.predict_proba(X_test)[:,1]
[23:34:59] WARNING: D:\bld\xgboost-split_1637426510059\work\src\learner.cc:576: Parameters: { "n_estimator" } might not be used. This could be a false alarm, with some parameters getting used by language bindings but then being mistakenly passed down to XGBoost core, or some parameter actually being used but getting flagged wrongly here. Please open an issue if you find any such cases. [0] validation_0-logloss:0.61352 [1] validation_0-logloss:0.54784 [2] validation_0-logloss:0.49425 [3] validation_0-logloss:0.44799 [4] validation_0-logloss:0.40911 [5] validation_0-logloss:0.37498 [6] validation_0-logloss:0.34571 [7] validation_0-logloss:0.32053 [8] validation_0-logloss:0.29721 [9] validation_0-logloss:0.27799 [10] validation_0-logloss:0.26030 [11] validation_0-logloss:0.24604 [12] validation_0-logloss:0.23156 [13] validation_0-logloss:0.22005 [14] validation_0-logloss:0.20857 [15] validation_0-logloss:0.19999 [16] validation_0-logloss:0.19012 [17] validation_0-logloss:0.18182 [18] validation_0-logloss:0.17473 [19] validation_0-logloss:0.16766 [20] validation_0-logloss:0.15820 [21] validation_0-logloss:0.15473 [22] validation_0-logloss:0.14895 [23] validation_0-logloss:0.14331 [24] validation_0-logloss:0.13634 [25] validation_0-logloss:0.13278 [26] validation_0-logloss:0.12791 [27] validation_0-logloss:0.12526 [28] validation_0-logloss:0.11998 [29] validation_0-logloss:0.11641 [30] validation_0-logloss:0.11450 [31] validation_0-logloss:0.11257 [32] validation_0-logloss:0.11154 [33] validation_0-logloss:0.10868 [34] validation_0-logloss:0.10668 [35] validation_0-logloss:0.10421 [36] validation_0-logloss:0.10296 [37] validation_0-logloss:0.10058 [38] validation_0-logloss:0.09868 [39] validation_0-logloss:0.09644 [40] validation_0-logloss:0.09587 [41] validation_0-logloss:0.09424 [42] validation_0-logloss:0.09471 [43] validation_0-logloss:0.09427 [44] validation_0-logloss:0.09389 [45] validation_0-logloss:0.09418 [46] validation_0-logloss:0.09402 [47] validation_0-logloss:0.09236 [48] validation_0-logloss:0.09301 [49] validation_0-logloss:0.09127 [50] validation_0-logloss:0.09005 [51] validation_0-logloss:0.08961 [52] validation_0-logloss:0.08958 [53] validation_0-logloss:0.09070 [54] validation_0-logloss:0.08958 [55] validation_0-logloss:0.09036 [56] validation_0-logloss:0.09159 [57] validation_0-logloss:0.09153 [58] validation_0-logloss:0.09199 [59] validation_0-logloss:0.09195 [60] validation_0-logloss:0.09194 [61] validation_0-logloss:0.09146 [62] validation_0-logloss:0.09031 [63] validation_0-logloss:0.08941 [64] validation_0-logloss:0.08972 [65] validation_0-logloss:0.08974 [66] validation_0-logloss:0.08962 [67] validation_0-logloss:0.08873 [68] validation_0-logloss:0.08862 [69] validation_0-logloss:0.08974 [70] validation_0-logloss:0.08998 [71] validation_0-logloss:0.08978 [72] validation_0-logloss:0.08958 [73] validation_0-logloss:0.08953 [74] validation_0-logloss:0.08875 [75] validation_0-logloss:0.08860 [76] validation_0-logloss:0.08812 [77] validation_0-logloss:0.08840 [78] validation_0-logloss:0.08874 [79] validation_0-logloss:0.08815 [80] validation_0-logloss:0.08758 [81] validation_0-logloss:0.08741 [82] validation_0-logloss:0.08849 [83] validation_0-logloss:0.08858 [84] validation_0-logloss:0.08807 [85] validation_0-logloss:0.08764 [86] validation_0-logloss:0.08742 [87] validation_0-logloss:0.08761 [88] validation_0-logloss:0.08707 [89] validation_0-logloss:0.08727 [90] validation_0-logloss:0.08716 [91] validation_0-logloss:0.08696 [92] validation_0-logloss:0.08717 [93] validation_0-logloss:0.08707 [94] validation_0-logloss:0.08659 [95] validation_0-logloss:0.08612 [96] validation_0-logloss:0.08714 [97] validation_0-logloss:0.08677 [98] validation_0-logloss:0.08669 [99] validation_0-logloss:0.08655
# 조기중단 성능 확인
get_clf_eval(y_test, ws100_preds, ws100_pred_proba)
오차행렬 [[34 3] [ 2 75]] 정확도: 0.9561, 정밀도: 0.9615, 재현율: 0.9740, F1: 0.9677, AUC:0.9947
조기중단 값을 너무 줄일 경우 충분한 학습이 되지 않아 예측 성능이 나빠질 수 있다¶
xgb_wrapper = XGBClassifier(n_estimator = 400, learning_rate = 0.1, max_depth = 3)
# 원칙적으로는 test데이터를 넣지 않고 train데이터를 나눈 validation데이터를 넣는다 !! 여기서는 데이터가 적기 때문에 이렇게 한 것!!
evals = [(X_test, y_test)]
xgb_wrapper.fit(X_train, y_train, early_stopping_rounds = 10, eval_metric = "logloss", eval_set=evals, verbose = True)
ws10_preds = xgb_wrapper.predict(X_test)
ws10_pred_proba = xgb_wrapper.predict_proba(X_test)[:,1]
[23:34:59] WARNING: D:\bld\xgboost-split_1637426510059\work\src\learner.cc:576: Parameters: { "n_estimator" } might not be used. This could be a false alarm, with some parameters getting used by language bindings but then being mistakenly passed down to XGBoost core, or some parameter actually being used but getting flagged wrongly here. Please open an issue if you find any such cases. [0] validation_0-logloss:0.61352 [1] validation_0-logloss:0.54784 [2] validation_0-logloss:0.49425 [3] validation_0-logloss:0.44799 [4] validation_0-logloss:0.40911 [5] validation_0-logloss:0.37498 [6] validation_0-logloss:0.34571 [7] validation_0-logloss:0.32053 [8] validation_0-logloss:0.29721 [9] validation_0-logloss:0.27799 [10] validation_0-logloss:0.26030 [11] validation_0-logloss:0.24604 [12] validation_0-logloss:0.23156 [13] validation_0-logloss:0.22005 [14] validation_0-logloss:0.20857 [15] validation_0-logloss:0.19999 [16] validation_0-logloss:0.19012 [17] validation_0-logloss:0.18182 [18] validation_0-logloss:0.17473 [19] validation_0-logloss:0.16766 [20] validation_0-logloss:0.15820 [21] validation_0-logloss:0.15473 [22] validation_0-logloss:0.14895 [23] validation_0-logloss:0.14331 [24] validation_0-logloss:0.13634 [25] validation_0-logloss:0.13278 [26] validation_0-logloss:0.12791 [27] validation_0-logloss:0.12526 [28] validation_0-logloss:0.11998 [29] validation_0-logloss:0.11641 [30] validation_0-logloss:0.11450 [31] validation_0-logloss:0.11257 [32] validation_0-logloss:0.11154 [33] validation_0-logloss:0.10868 [34] validation_0-logloss:0.10668 [35] validation_0-logloss:0.10421 [36] validation_0-logloss:0.10296 [37] validation_0-logloss:0.10058 [38] validation_0-logloss:0.09868 [39] validation_0-logloss:0.09644 [40] validation_0-logloss:0.09587 [41] validation_0-logloss:0.09424 [42] validation_0-logloss:0.09471 [43] validation_0-logloss:0.09427 [44] validation_0-logloss:0.09389 [45] validation_0-logloss:0.09418 [46] validation_0-logloss:0.09402 [47] validation_0-logloss:0.09236 [48] validation_0-logloss:0.09301 [49] validation_0-logloss:0.09127 [50] validation_0-logloss:0.09005 [51] validation_0-logloss:0.08961 [52] validation_0-logloss:0.08958 [53] validation_0-logloss:0.09070 [54] validation_0-logloss:0.08958 [55] validation_0-logloss:0.09036 [56] validation_0-logloss:0.09159 [57] validation_0-logloss:0.09153 [58] validation_0-logloss:0.09199 [59] validation_0-logloss:0.09195 [60] validation_0-logloss:0.09194 [61] validation_0-logloss:0.09146
get_clf_eval(y_test, ws10_preds, ws10_pred_proba) # ????
오차행렬 [[34 3] [ 2 75]] 정확도: 0.9561, 정밀도: 0.9615, 재현율: 0.9740, F1: 0.9677, AUC:0.9947
중요한 피처를 쉽게 확인할수 있다 ( 시각화 )
f뒤에 붙어 있는 숫자가 몇번째 피처인지를 알려준다. ex) f0은 첫번째 피처, f1은 두번째 피처...
from xgboost import plot_importance
import matplotlib.pyplot as plt
%matplotlib inline
fig, ax = plt.subplots(figsize = (10, 12))
# 사이킷런의 Wrapper 클래스를 입력해도 된다
plot_importance(xgb_wrapper, ax=ax)
<AxesSubplot:title={'center':'Feature importance'}, xlabel='F score', ylabel='Features'>
7. LightGBM¶
XGBoost가 여전히 학습시간이 길어 더 시간을 빠르게 하기 위해 개발..!! ( XGBoost가 나오고 2년뒤에 개발되었다 )
장점 : 더 빠른 학습, 예측 수행시간, 더 작은 메모리 사용량, 카테고리형 피처의 자동변환과 최적 분할 ( one-hot을 사용하지 않아도 된다 ! )
단점 : 적은 데이터 세트에 적용시 과적합이 발생하기 쉽다.
특징 : 리프중심의 트리 분할을 사용하여 최대 손실값을 가지는 리프노드를 지속적으로 분할한다
( 따라서 과적합에 강하기 위해 균형 트리 분할을 사용하는 일반 트리 계열들보다 시간이 짧게 걸린다 )
LightGBM 주요 파라미터
- n_estimator : 반복하려는 트리의 개수 ( default = 100 )
- learning_rate : 학습률 ( default = 0.1 )
- max_depth : 트리의 깊이 ( default = -1, 0보다 작은값을 지정하면 깊이에 제한이 없음 )
- min_child_samples : 리프노드가 되기 위해서 최소한으로 필요한 최소 레코드 수 ( default = 20 )
- num_leaves : 하나의 트리가 가질 수 있는 최대 리프 개수 ( default = 31 )
- boosting : 부스팅의 트리를 생성하는 알고리즘 gbdt, goss, rf 등등... ( default = 'gbdt')
- subsample : 과적합을 막기 위해 데이터를 샘플링하는 비율 ( default= 1 )
- colsample_bytree : 개별 트리를 학습할 때마다 무작위로 선택하는 피처의 비율 ( default = 1 )
- reg_lambda : L2 규제 ( default = 0 )
- reg_alpha : L1 규제 ( default = 0 )
- objective : 최솟값을 가져야할 손실함수를 정의한다. ( 회귀, multi, binary에 따라 손실함수가 지정된다 )
일반적인 튜닝방법
- num_leaves의 개수를 중심으로 min_child_samples, max_depth를 함께 조정하며 모델의 복잡도를 줄이는 것이 기본이다.
- learning_rate를 줄이며 n_estimator를 증가시키는 것은 부스팅 계열에서 기본적인 튜닝방법!!
- 이외에도 과적합 제어를 위해 reg_lambda, reg_alpha 등의 규제와 colsample_bytree, subsample로 데이터에 사용할 피처의 개수나 데이터 샘플링 레코드 수를 줄이는 것도 좋은 방법이다.
from lightgbm import LGBMClassifier
ftr = dataset.data
target = dataset.target
X_train, X_test, y_train, y_test = train_test_split(ftr, target, test_size = 0.2, random_state = 156)
lgbm_wrapper = LGBMClassifier(n_estimators=400)
# lgbm또한 조기중단을 수행할 수 있다.
evals = [(X_test, y_test)]
lgbm_wrapper.fit(X_train, y_train, early_stopping_rounds = 100, eval_metric = "logloss", eval_set=evals, verbose=True)
preds = lgbm_wrapper.predict(X_test)
pred_proba = lgbm_wrapper.predict_proba(X_test)[:,1]
[1] valid_0's binary_logloss: 0.565079 [2] valid_0's binary_logloss: 0.507451 [3] valid_0's binary_logloss: 0.458489 [4] valid_0's binary_logloss: 0.417481 [5] valid_0's binary_logloss: 0.385507 [6] valid_0's binary_logloss: 0.355773 [7] valid_0's binary_logloss: 0.329587 [8] valid_0's binary_logloss: 0.308478 [9] valid_0's binary_logloss: 0.285395 [10] valid_0's binary_logloss: 0.267055 [11] valid_0's binary_logloss: 0.252013 [12] valid_0's binary_logloss: 0.237018 [13] valid_0's binary_logloss: 0.224756 [14] valid_0's binary_logloss: 0.213383 [15] valid_0's binary_logloss: 0.203058 [16] valid_0's binary_logloss: 0.194015 [17] valid_0's binary_logloss: 0.186412 [18] valid_0's binary_logloss: 0.179108 [19] valid_0's binary_logloss: 0.174004 [20] valid_0's binary_logloss: 0.167155 [21] valid_0's binary_logloss: 0.162494 [22] valid_0's binary_logloss: 0.156886 [23] valid_0's binary_logloss: 0.152855 [24] valid_0's binary_logloss: 0.151113 [25] valid_0's binary_logloss: 0.148395 [26] valid_0's binary_logloss: 0.145869 [27] valid_0's binary_logloss: 0.143036 [28] valid_0's binary_logloss: 0.14033 [29] valid_0's binary_logloss: 0.139609 [30] valid_0's binary_logloss: 0.136109 [31] valid_0's binary_logloss: 0.134867 [32] valid_0's binary_logloss: 0.134729 [33] valid_0's binary_logloss: 0.1311 [34] valid_0's binary_logloss: 0.131143 [35] valid_0's binary_logloss: 0.129435 [36] valid_0's binary_logloss: 0.128474 [37] valid_0's binary_logloss: 0.126683 [38] valid_0's binary_logloss: 0.126112 [39] valid_0's binary_logloss: 0.122831 [40] valid_0's binary_logloss: 0.123162 [41] valid_0's binary_logloss: 0.125592 [42] valid_0's binary_logloss: 0.128293 [43] valid_0's binary_logloss: 0.128123 [44] valid_0's binary_logloss: 0.12789 [45] valid_0's binary_logloss: 0.122818 [46] valid_0's binary_logloss: 0.12496 [47] valid_0's binary_logloss: 0.125578 [48] valid_0's binary_logloss: 0.127381 [49] valid_0's binary_logloss: 0.128349 [50] valid_0's binary_logloss: 0.127004 [51] valid_0's binary_logloss: 0.130288 [52] valid_0's binary_logloss: 0.131362 [53] valid_0's binary_logloss: 0.133363 [54] valid_0's binary_logloss: 0.1332 [55] valid_0's binary_logloss: 0.134543 [56] valid_0's binary_logloss: 0.130803 [57] valid_0's binary_logloss: 0.130306 [58] valid_0's binary_logloss: 0.132514 [59] valid_0's binary_logloss: 0.133278 [60] valid_0's binary_logloss: 0.134804 [61] valid_0's binary_logloss: 0.136888 [62] valid_0's binary_logloss: 0.138745 [63] valid_0's binary_logloss: 0.140497 [64] valid_0's binary_logloss: 0.141368 [65] valid_0's binary_logloss: 0.140764 [66] valid_0's binary_logloss: 0.14348 [67] valid_0's binary_logloss: 0.143418 [68] valid_0's binary_logloss: 0.143682 [69] valid_0's binary_logloss: 0.145076 [70] valid_0's binary_logloss: 0.14686 [71] valid_0's binary_logloss: 0.148051 [72] valid_0's binary_logloss: 0.147664 [73] valid_0's binary_logloss: 0.149478 [74] valid_0's binary_logloss: 0.14708 [75] valid_0's binary_logloss: 0.14545 [76] valid_0's binary_logloss: 0.148767 [77] valid_0's binary_logloss: 0.149959 [78] valid_0's binary_logloss: 0.146083 [79] valid_0's binary_logloss: 0.14638 [80] valid_0's binary_logloss: 0.148461 [81] valid_0's binary_logloss: 0.15091 [82] valid_0's binary_logloss: 0.153011 [83] valid_0's binary_logloss: 0.154807 [84] valid_0's binary_logloss: 0.156501 [85] valid_0's binary_logloss: 0.158586 [86] valid_0's binary_logloss: 0.159819 [87] valid_0's binary_logloss: 0.161745 [88] valid_0's binary_logloss: 0.162829 [89] valid_0's binary_logloss: 0.159142 [90] valid_0's binary_logloss: 0.156765 [91] valid_0's binary_logloss: 0.158625 [92] valid_0's binary_logloss: 0.156832 [93] valid_0's binary_logloss: 0.154616 [94] valid_0's binary_logloss: 0.154263 [95] valid_0's binary_logloss: 0.157156 [96] valid_0's binary_logloss: 0.158617 [97] valid_0's binary_logloss: 0.157495 [98] valid_0's binary_logloss: 0.159413 [99] valid_0's binary_logloss: 0.15847 [100] valid_0's binary_logloss: 0.160746 [101] valid_0's binary_logloss: 0.16217 [102] valid_0's binary_logloss: 0.165293 [103] valid_0's binary_logloss: 0.164749 [104] valid_0's binary_logloss: 0.167097 [105] valid_0's binary_logloss: 0.167697 [106] valid_0's binary_logloss: 0.169462 [107] valid_0's binary_logloss: 0.169947 [108] valid_0's binary_logloss: 0.171 [109] valid_0's binary_logloss: 0.16907 [110] valid_0's binary_logloss: 0.169521 [111] valid_0's binary_logloss: 0.167719 [112] valid_0's binary_logloss: 0.166648 [113] valid_0's binary_logloss: 0.169053 [114] valid_0's binary_logloss: 0.169613 [115] valid_0's binary_logloss: 0.170059 [116] valid_0's binary_logloss: 0.1723 [117] valid_0's binary_logloss: 0.174733 [118] valid_0's binary_logloss: 0.173526 [119] valid_0's binary_logloss: 0.1751 [120] valid_0's binary_logloss: 0.178254 [121] valid_0's binary_logloss: 0.182968 [122] valid_0's binary_logloss: 0.179017 [123] valid_0's binary_logloss: 0.178326 [124] valid_0's binary_logloss: 0.177149 [125] valid_0's binary_logloss: 0.179171 [126] valid_0's binary_logloss: 0.180948 [127] valid_0's binary_logloss: 0.183861 [128] valid_0's binary_logloss: 0.187579 [129] valid_0's binary_logloss: 0.188122 [130] valid_0's binary_logloss: 0.1857 [131] valid_0's binary_logloss: 0.187442 [132] valid_0's binary_logloss: 0.188578 [133] valid_0's binary_logloss: 0.189729 [134] valid_0's binary_logloss: 0.187313 [135] valid_0's binary_logloss: 0.189279 [136] valid_0's binary_logloss: 0.191068 [137] valid_0's binary_logloss: 0.192414 [138] valid_0's binary_logloss: 0.191255 [139] valid_0's binary_logloss: 0.193453 [140] valid_0's binary_logloss: 0.196969 [141] valid_0's binary_logloss: 0.196378 [142] valid_0's binary_logloss: 0.196367 [143] valid_0's binary_logloss: 0.19869 [144] valid_0's binary_logloss: 0.200352 [145] valid_0's binary_logloss: 0.19712
get_clf_eval(y_test, preds, pred_proba)
오차행렬 [[33 4] [ 1 76]] 정확도: 0.9561, 정밀도: 0.9500, 재현율: 0.9870, F1: 0.9682, AUC:0.9905
# light gbm도 마찬가지로 피처 중요도를 시각화할 수 있다.
from lightgbm import plot_importance
fig, ax = plt.subplots(figsize = (10, 12))
plot_importance(lgbm_wrapper, ax=ax)
<AxesSubplot:title={'center':'Feature importance'}, xlabel='Feature importance', ylabel='Features'>
8. 실습 - 산탄데르 고객 만족 예측¶
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
cust_df = pd.read_csv('./train_santander.csv', encoding = 'latin-1')
print('dataset shape:', cust_df.shape)
cust_df.head(3)
# 타겟값을 포함한 371개 피처 존재
dataset shape: (76020, 371)
ID | var3 | var15 | imp_ent_var16_ult1 | imp_op_var39_comer_ult1 | imp_op_var39_comer_ult3 | imp_op_var40_comer_ult1 | imp_op_var40_comer_ult3 | imp_op_var40_efect_ult1 | imp_op_var40_efect_ult3 | ... | saldo_medio_var33_hace2 | saldo_medio_var33_hace3 | saldo_medio_var33_ult1 | saldo_medio_var33_ult3 | saldo_medio_var44_hace2 | saldo_medio_var44_hace3 | saldo_medio_var44_ult1 | saldo_medio_var44_ult3 | var38 | TARGET | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 23 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 39205.17 | 0 |
1 | 3 | 2 | 34 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 49278.03 | 0 |
2 | 4 | 2 | 23 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | ... | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 67333.77 | 0 |
3 rows × 371 columns
cust_df.describe()
# Nan값을 -999999로 변환햇을 것! -> 최빈값인 2로 변경
ID | var3 | var15 | imp_ent_var16_ult1 | imp_op_var39_comer_ult1 | imp_op_var39_comer_ult3 | imp_op_var40_comer_ult1 | imp_op_var40_comer_ult3 | imp_op_var40_efect_ult1 | imp_op_var40_efect_ult3 | ... | saldo_medio_var33_hace2 | saldo_medio_var33_hace3 | saldo_medio_var33_ult1 | saldo_medio_var33_ult3 | saldo_medio_var44_hace2 | saldo_medio_var44_hace3 | saldo_medio_var44_ult1 | saldo_medio_var44_ult3 | var38 | TARGET | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | ... | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 76020.000000 | 7.602000e+04 | 76020.000000 |
mean | 75964.050723 | -1523.199277 | 33.212865 | 86.208265 | 72.363067 | 119.529632 | 3.559130 | 6.472698 | 0.412946 | 0.567352 | ... | 7.935824 | 1.365146 | 12.215580 | 8.784074 | 31.505324 | 1.858575 | 76.026165 | 56.614351 | 1.172358e+05 | 0.039569 |
std | 43781.947379 | 39033.462364 | 12.956486 | 1614.757313 | 339.315831 | 546.266294 | 93.155749 | 153.737066 | 30.604864 | 36.513513 | ... | 455.887218 | 113.959637 | 783.207399 | 538.439211 | 2013.125393 | 147.786584 | 4040.337842 | 2852.579397 | 1.826646e+05 | 0.194945 |
min | 1.000000 | -999999.000000 | 5.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 5.163750e+03 | 0.000000 |
25% | 38104.750000 | 2.000000 | 23.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 6.787061e+04 | 0.000000 |
50% | 76043.000000 | 2.000000 | 28.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.064092e+05 | 0.000000 |
75% | 113748.750000 | 2.000000 | 40.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | ... | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 1.187563e+05 | 0.000000 |
max | 151838.000000 | 238.000000 | 105.000000 | 210000.000000 | 12888.030000 | 21024.810000 | 8237.820000 | 11073.570000 | 6600.000000 | 6600.000000 | ... | 50003.880000 | 20385.720000 | 138831.630000 | 91778.730000 | 438329.220000 | 24650.010000 | 681462.900000 | 397884.300000 | 2.203474e+07 | 1.000000 |
8 rows × 371 columns
cust_df['var3'].replace(-999999,2,inplace=True)
cust_df.drop('ID', axis=1, inplace=True)
# X와 y분리
X_features = cust_df.iloc[:,:-1]
y_labels = cust_df.iloc[:,-1]
print('피처 데이터 shape:{0}'.format(X_features.shape))
피처 데이터 shape:(76020, 369)
XGBoost, LGBM 실습은 생략
9. 실습 - 캐글 신용카드 사기 검출¶
# 매우 불균형한 타겟값을 가지고 있는 데이터
# 1이 사기 데이터이다.
pd.read_csv('./creditcard.csv').Class.value_counts()
0 284315 1 492 Name: Class, dtype: int64
언더 샘플링과 오버 샘플링
레이블이 불균형한 분포를 가진 데이터 세트를 학습시킬 때 예측 성능의 문제가 발생할 수 있다.
일방적으로 0에 치우친 학습을 수행하게 되어 데이터 검출이 어려워지기 쉽기 때문!
-> 언더 샘플링과 오버 샘플링 ( 일반적으로는 오버 샘플링이 더 좋은 경우가 많다 )
출처 : https://www.kaggle.com/rafjaa/resampling-strategies-for-imbalanced-datasets
언더 샘플링 : 많은 데이터 세트를 적은 데이터 세트 수준으로 감소시키는 방식으로 정상 레이블이 10000건, 이상 레이블이 100건일 경우 정상 레이블을 100건으로 줄여버리는 방식이다.
( 너무 많은 정상 레이블을 감소시키기 때문에 정상 레이블이 오히려 제대로 된 학습을 수행할 수 없다는 단점 )
오버 샘플링 : 이상 데이터와 같이 적은 데이터 세트를 증식하여 학습을 위한 충분한 데이터를 확보하는 방법. 동일한 데이터를 단순히 증식하는 것은 과적합이 되므로 원본 데이터의 피처 값들을 아주 약간만 변경하여 증식한다.
SMOTE 방법 ( Synthetic Minority Over-sampling Technique )
적은 데이터 세트에 있는 개별 데이터들의 K-최근접 이웃을 찾아서 데이터와 K개 이웃들의 차이를 일정값으로 만든 뒤 기존 데이터와 약간 차이가 나는 새로운 데이터들을 생성하는 방식
python에서 smote 구현 -> imbalanced-learn 패키지
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
%matplotlib inline
card_df = pd.read_csv('./creditcard.csv')
card_df.head(3)
# 여기서 V피처들의 의미는 알 수 없다.
# Amount는 신용카드 트랜잭션 금액
# Class는 0이 정상, 1이 사기
Time | V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | ... | V21 | V22 | V23 | V24 | V25 | V26 | V27 | V28 | Amount | Class | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0 | -1.359807 | -0.072781 | 2.536347 | 1.378155 | -0.338321 | 0.462388 | 0.239599 | 0.098698 | 0.363787 | ... | -0.018307 | 0.277838 | -0.110474 | 0.066928 | 0.128539 | -0.189115 | 0.133558 | -0.021053 | 149.62 | 0 |
1 | 0.0 | 1.191857 | 0.266151 | 0.166480 | 0.448154 | 0.060018 | -0.082361 | -0.078803 | 0.085102 | -0.255425 | ... | -0.225775 | -0.638672 | 0.101288 | -0.339846 | 0.167170 | 0.125895 | -0.008983 | 0.014724 | 2.69 | 0 |
2 | 1.0 | -1.358354 | -1.340163 | 1.773209 | 0.379780 | -0.503198 | 1.800499 | 0.791461 | 0.247676 | -1.514654 | ... | 0.247998 | 0.771679 | 0.909412 | -0.689281 | -0.327642 | -0.139097 | -0.055353 | -0.059752 | 378.66 | 0 |
3 rows × 31 columns
card_df.info() # 결측치 없음
<class 'pandas.core.frame.DataFrame'> RangeIndex: 284807 entries, 0 to 284806 Data columns (total 31 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Time 284807 non-null float64 1 V1 284807 non-null float64 2 V2 284807 non-null float64 3 V3 284807 non-null float64 4 V4 284807 non-null float64 5 V5 284807 non-null float64 6 V6 284807 non-null float64 7 V7 284807 non-null float64 8 V8 284807 non-null float64 9 V9 284807 non-null float64 10 V10 284807 non-null float64 11 V11 284807 non-null float64 12 V12 284807 non-null float64 13 V13 284807 non-null float64 14 V14 284807 non-null float64 15 V15 284807 non-null float64 16 V16 284807 non-null float64 17 V17 284807 non-null float64 18 V18 284807 non-null float64 19 V19 284807 non-null float64 20 V20 284807 non-null float64 21 V21 284807 non-null float64 22 V22 284807 non-null float64 23 V23 284807 non-null float64 24 V24 284807 non-null float64 25 V25 284807 non-null float64 26 V26 284807 non-null float64 27 V27 284807 non-null float64 28 V28 284807 non-null float64 29 Amount 284807 non-null float64 30 Class 284807 non-null int64 dtypes: float64(30), int64(1) memory usage: 67.4 MB
from sklearn.model_selection import train_test_split
# 입력받은 DataFrame을 복사한 뒤 Time칼럼을 삭제하고 복사된 DataFrame반환
def get_preprocessed_df(df = None):
df_copy = df.copy()
df_copy.drop('Time', axis = 1, inplace=True) # Time열을 지워라
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]
# Stratified 기반 분할 ( y_target값이 불균형하기 때문!! )
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('학습 데이터 레이블 값 비율')
print(y_train.value_counts()/y_train.shape[0]*100)
print()
print('테스트 데이터 레이블 값 비율')
print(y_test.value_counts()/y_test.shape[0]*100)
# train과 test값이 비슷한 비율로 stratified하게 나뉜 것을 확인할 수 있다.
학습 데이터 레이블 값 비율 0 99.827451 1 0.172549 Name: Class, dtype: float64 테스트 데이터 레이블 값 비율 0 99.826785 1 0.173215 Name: Class, dtype: float64
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]
get_clf_eval(y_test, lr_pred, lr_pred_proba) # 책의 내용과 다른 이유?
오차행렬 [[85282 13] [ 56 92]] 정확도: 0.9992, 정밀도: 0.8762, 재현율: 0.6216, F1: 0.7273, AUC:0.9592
# 인자로 sklearn의 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)
LGBM의 boost_from_average의 디폴트 값이 True로 변경되어 레이블 값이 극도로 불균형한 분포를 이루는 경우 True설정은 ROC-AUC 성능을 매우 크게 저하시킵니다. -> False로 설정 필요!!
레이블 값이 불균형한 데이터를 다룰 때 lgbm의 boost_from_average = False로 설정시 성능이 높아질 것을 보고 기존에 공모전에서 사용했던 레이블 값이 1과 0의 분포가 큰 차이가 있는 데이터에 사용해보았다. -> 결과는 실패!!
from lightgbm import LGBMClassifier
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)
# Logistic 보다는 좋은 수치
오차행렬 [[85290 5] [ 36 112]] 정확도: 0.9995, 정밀도: 0.9573, 재현율: 0.7568, F1: 0.8453, AUC:0.9790
데이터 분포도 변환 후 모델 학습 / 예측 / 평가
# 왜곡된 분포도를 가지는 데이터를 재가공한 뒤에 다시 테스트
# 대부분의 선형 모델은 중요 피처들의 값이 정규 분포 형태를 유지하는 것을 선호한다.
# 중요해보이는 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()
from sklearn.preprocessing import StandardScaler
# 정규분포 형태로 변환
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로 피처명 변경하고 맨 앞 칼럼으로 입력
df_copy.insert(0, 'Amount_Scaled', amount_n)
# 기존 Time, Amount피처 삭제
df_copy.drop(['Time', 'Amount'], axis = 1, inplace = True)
return df_copy
# Amount를 정규분포 형태로 변환
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()
print('### LIghtGBM 예측 성능 ###')
lgbm_clf = LGBMClassifier(n_estimators = 1000, num_leaves = 64, n_jobs = -1)
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test )
# 다른 lgbm 결과값??
### 로지스틱 회귀 예측 성능 ### 오차행렬 [[85281 14] [ 58 90]] 정확도: 0.9992, 정밀도: 0.8654, 재현율: 0.6081, F1: 0.7143, AUC:0.9702 ### LIghtGBM 예측 성능 ### 오차행렬 [[85256 39] [ 121 27]] 정확도: 0.9981, 정밀도: 0.4091, 재현율: 0.1824, F1: 0.2523, AUC:0.5943
# 로그 변환
def get_preprocessed_df(df=None):
df_copy = df.copy()
# numpy의 log1p()를 이용
amount_n = np.log1p(df_copy['Amount'])
# 변환된 Amount를 Amount_Scaled로 피처명 변경하고 맨 앞 칼럼으로 입력
df_copy.insert(0, 'Amount_Scaled', amount_n)
# 기존 Time, Amount피처 삭제
df_copy.drop(['Time', 'Amount'], axis = 1, inplace = True)
return df_copy
# Amount를 로그 변환
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()
print('### LIghtGBM 예측 성능 ###')
lgbm_clf = LGBMClassifier(n_estimators = 1000, num_leaves = 64, n_jobs = -1)
get_model_train_eval(lgbm_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test )
### 로지스틱 회귀 예측 성능 ### 오차행렬 [[85283 12] [ 59 89]] 정확도: 0.9992, 정밀도: 0.8812, 재현율: 0.6014, F1: 0.7149, AUC:0.9727 ### LIghtGBM 예측 성능 ### 오차행렬 [[85219 76] [ 58 90]] 정확도: 0.9984, 정밀도: 0.5422, 재현율: 0.6081, F1: 0.5732, AUC:0.8069
이상치 데이터¶
Outlier는 일반적으로 IQR 방식을 적용하여 찾아낸다. Q1 ~ Q3을 IQR 구간이라고 하고 IQR에서 1.5를 곱해서 생성된 범위를 이용해 최댓값과 최솟값의 범위를 벗어난 데이터를 이상치로 간주한다.
이상치 데이터를 확인할 때 많은 피처가 있을 경우 모든 피처들의 이상치를 검출하는 것은 시간이 많이 소모되기 때문에 결정값이나 상관성이 높지 않은 피처들의 경우에는 이상치를 제거하더라도 크게 성능 향상이 되지 않기 때문에 레이블과 상관성이 높은 피처들을 위주로 이상치를 검출하는 것이 좋다!!!
# 상관성 확인
import seaborn as sns
plt.figure(figsize = (9,9))
corr = card_df.corr()
sns.heatmap(corr, cmap = 'RdBu')
# 양의 상관관계가 높을 수록 진한 파란색, 음의 상관관계가 높을 수록 진한 빨간색
plt.show()
# Class와의 상관관계를 확인!! ( V14, V17의 음의 상관관계가 높다 )
# 상관관계가 높은 V14와 V17의 이상치 제거
import numpy as np
def get_outlier(df = None, column = None, weight = 1.5):
# fraud에 해당하는 column만 추출, IQR은 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
outlier_index = get_outlier(df = card_df, column='V14', weight = 1.5)
print('이상치 데이터 인덱스 : ', outlier_index)
이상치 데이터 인덱스 : Int64Index([8296, 8615, 9035, 9252], dtype='int64')
이상치 데이터를 삭제하는 로직을 get_preprocessed_df에 추가 -> 다시 lgbm을 돌림 ( 생략 )
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 적용 전 레이블 값 분포 : ')
print(pd.Series(y_train).value_counts())
print('SMOTE 적용 후 학습용 피처/레이블 데이터 세트 :', X_train_over.shape, y_train_over.shape)
print('SMOTE 적용 후 레이블 값 분포 : ')
print(pd.Series(y_train_over).value_counts())
SMOTE 적용 전 학습용 피처/레이블 데이터 세트 : (199364, 29) (199364,) SMOTE 적용 전 레이블 값 분포 : 0 199020 1 344 Name: Class, dtype: int64 SMOTE 적용 후 학습용 피처/레이블 데이터 세트 : (398040, 29) (398040,) SMOTE 적용 후 레이블 값 분포 : 0 199020 1 199020 Name: Class, dtype: int64
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)
# 재현율이 높아지지만 정밀도, F1이 박살이 난다
오차행렬 [[83317 1978] [ 15 133]] 정확도: 0.9767, 정밀도: 0.0630, 재현율: 0.8986, F1: 0.1178, AUC:0.9803
너무 많은 Class = 1 데이터를 학습하면서 실제 테스트 데이터 세트에서 예측을 지나치게 Class = 1로 적용해 정밀도가 떨어지게 된것!
from sklearn.metrics import precision_recall_curve
def precision_recall_curve_plot(y_test, pred_proba_c1):
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], ls = '--', label = 'precision')
plt.plot(thresholds, recalls[0:threshold_boundary], label = 'recall')
# threshold 값 X축의 Scale을 0.1 단위로 변경
start, end = plt.xlim() # 좋은 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를 적용하면 재현율은 높아지나 정밀도는 낮아진다!! ( 좋은 SMOTE 패키지 일수록 재현율 증가율은 높이고 정밀도 감소율은 낮출 수 있게.. )
SMOTE로 오버 샘플링을 사용하면 비대칭 적인 레이블 데이터를 가졌던 저번 공모전 데이터에서 더 높은 점수를 얻을 수 있었을 지 궁금해 위의 boost_from_average = False와 같이 사용해 성능이 올라가는지를 확인해보았지만 결과는 다시 실패!!
10. 스태킹 앙상블¶
import numpy as np
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
cancer_data = load_breast_cancer()
X_data = cancer_data.data
y_label = cancer_data.target
X_train, X_test, y_train, y_test = train_test_split(X_data, y_label, test_size=0.2, random_state = 0)
# 개별 ML 모델 생성
knn_clf = KNeighborsClassifier(n_neighbors = 4)
rf_clf = RandomForestClassifier(n_estimators = 100, random_state = 0)
dt_clf = DecisionTreeClassifier()
ada_clf = AdaBoostClassifier(n_estimators = 100)
# 스태킹으로 만들어진 데이터 세트를 학습, 예측할 최종 모델
lr_final = LogisticRegression(C=10)
knn_clf.fit(X_train, y_train)
rf_clf.fit(X_train, y_train)
dt_clf.fit(X_train, y_train)
ada_clf.fit(X_train, y_train)
AdaBoostClassifier(n_estimators=100)
knn_pred = knn_clf.predict(X_test)
rf_pred = rf_clf.predict(X_test)
dt_pred = dt_clf.predict(X_test)
ada_pred = ada_clf.predict(X_test)
print('KNN 정확도 : {0:.4f}'.format(accuracy_score(y_test, knn_pred)))
print('rf 정확도 : {0:.4f}'.format(accuracy_score(y_test, rf_pred)))
print('DT 정확도 : {0:.4f}'.format(accuracy_score(y_test, dt_pred)))
print('Adaboost 정확도 : {0:.4f}'.format(accuracy_score(y_test, ada_pred)))
KNN 정확도 : 0.9211 rf 정확도 : 0.9649 DT 정확도 : 0.9123 Adaboost 정확도 : 0.9561
스태킹 : 개별 알고리즘으로부터 예측된 예측값을 칼럼 레벨로 옆으로 붙여서 피처 값으로 만듦 -> 최종 메타 모델의 학습 데이터로 다시 사용한다!!
예측 데이터 세트는 1차원 형태의 ndarray이기 때문에 transpose를 이용해 행과 열 위치를 바꾼 ndarray로 변환
pred = np.array([knn_pred, rf_pred, dt_pred, ada_pred])
print(pred.shape)
# transpose를 이용해 행과 열의 위치 교환, 칼럼 레벨로 각 알고리즘의 예측 결과를 피처로 만듦
pred = np.transpose(pred)
print(pred.shape)
(4, 114) (114, 4)
lr_final.fit(pred, y_test)
final = lr_final.predict(pred)
print('최종 메타 모델의 예측 정확도 : ', accuracy_score(y_test, final))
최종 메타 모델의 예측 정확도 : 0.9649122807017544
과적합을 개선하기 위한 CV 세트 기반의 스태킹 모델
과적합을 개선하기 위해 최종 메타 모델을 위한 데이터 세트를 만들 때 교차 검증 기반으로 예측된 결과 데이터 세트를 이용한다. ( train을 -> train과 validation으로 나눈다는 것 !!, test를 바로 학습할경우 과적합 문제가 발생할 수 있다 )
- 각 모델별로 학습 / 테스트 데이터를 예측한 결과 값을 기반으로 메타 모델을 위한 학습 / 테스트용 데이터를 생성
- 스텝 1에서 개별 모델들이 생성한 학습용 데이터를 모두 스태킹 형태로 합쳐서 모델이 학습할 최종 학습용 데이터 세트를 생성 -> 각 모델들이 생성한 테스트용 데이터를 모두 스태킹 형태로 합쳐서 메타 모델이 예측할 최종 테스트 데이터 세트를 생성 ( 최종적으로 생성된 테스트 데이터 세트를 예측하고 평가는 원본 테스트 데이터의 레이블 데이터를 기반으로 진행한다!! )
<KFold 참고자료>
https://jonsyou.tistory.com/23
from sklearn.model_selection import KFold
from sklearn.metrics import mean_absolute_error
# 개별 기반 모델에서 최종 메타 모델이 사용할 학습 및 테스트용 데이터를 생성하기 위한 함수
def get_stacking_base_datasets(model, X_train_n, y_train_n, X_test_n, n_folds):
# 지정된 n_folds값으로 KFold 생성
kf = KFold(n_splits = n_folds, shuffle = False) # random_state 사용시 Setting a random_state has no effect since shuffle is False오류
# 추후에 메타 모델이 사용할 학습 데이터 반환을 위한 넘파이 배열 초기화
train_fold_pred = np.zeros((X_train_n.shape[0],1))
# n_folds의 개수만큼 최종 메타 모델이 반환할 넘파이 배열을 만든다
test_pred = np.zeros((X_test_n.shape[0], n_folds))
print(model.__class__.__name__, 'model 시작')
for folder_counter, (train_index, valid_index) in enumerate(kf.split(X_train_n)):
# 입력된 학습 데이터에서 기반 모델이 학습 / 예측할 폴드 데이터 세트 추출
print('\t 폴드 세트 : ', folder_counter, ' 시작')
X_tr = X_train_n[train_index]
y_tr = y_train_n[train_index]
X_te = X_train_n[valid_index]
# 폴드 세트 내부에서 다시 만들어진 학습 데이터로 기반 모델의 학습 수행
model.fit(X_tr, y_tr)
# 폴드 세트 내부에서 다시 만들어진 검증 데이터로 기반 모델 예측 후 데이터 저장
train_fold_pred[valid_index, :] = model.predict(X_te).reshape(-1,1)
# 입력된 원본 테스트 데이터를 폴드 세트내 학습된 기반 모델에서 예측 후 데이터 저장
# test_pred는 n_folds개수의 열을 가진 dataframe이므로 각 열에 맞는 결과를 넣어주는 것!
test_pred[:, folder_counter] = model.predict(X_test_n)
# 폴드 세트 내에서 원본 테스트 데이터를 예측한 데이터를 평균하여 테스트 데이터로 생성
test_pred_mean = np.mean(test_pred, axis=1).reshape(-1,1)
# train_fold_pred는 최종 메타 모델이 사용하는 학습 데이터, test_pred_mean은 테스트 데이터
return train_fold_pred, test_pred_mean
train은 본래 데이터를 cv하며 train-valid한 결과를 각행에 저장한 데이터, test는 cv를 진행하며 그 결과를 열마다 저장하고 평균해서 하나의 column으로 결과를 낸 데이터
# step 1
knn_train, knn_test = get_stacking_base_datasets(knn_clf, X_train, y_train, X_test, 7)
rf_train, rf_test = get_stacking_base_datasets(rf_clf, X_train, y_train, X_test, 7)
dt_train, dt_test = get_stacking_base_datasets(dt_clf, X_train, y_train, X_test, 7)
ada_train, ada_test = get_stacking_base_datasets(ada_clf, X_train, y_train, X_test, 7)
KNeighborsClassifier model 시작 폴드 세트 : 0 시작 폴드 세트 : 1 시작 폴드 세트 : 2 시작 폴드 세트 : 3 시작 폴드 세트 : 4 시작 폴드 세트 : 5 시작 폴드 세트 : 6 시작 RandomForestClassifier model 시작 폴드 세트 : 0 시작 폴드 세트 : 1 시작 폴드 세트 : 2 시작 폴드 세트 : 3 시작 폴드 세트 : 4 시작 폴드 세트 : 5 시작 폴드 세트 : 6 시작 DecisionTreeClassifier model 시작 폴드 세트 : 0 시작 폴드 세트 : 1 시작 폴드 세트 : 2 시작 폴드 세트 : 3 시작 폴드 세트 : 4 시작 폴드 세트 : 5 시작 폴드 세트 : 6 시작 AdaBoostClassifier model 시작 폴드 세트 : 0 시작 폴드 세트 : 1 시작 폴드 세트 : 2 시작 폴드 세트 : 3 시작 폴드 세트 : 4 시작 폴드 세트 : 5 시작 폴드 세트 : 6 시작
# 개별 모델로부터 나온 y_train 예측값들 열로 합치기
Stack_final_X_train = np.concatenate((knn_train,rf_train,dt_train,ada_train), axis=1)
# 개별 모델로부터 나온 y_test 예측값들 열로 합치기
Stack_final_X_test = np.concatenate((knn_test,rf_test,dt_test,ada_test), axis=1)
lr_final.fit(Stack_final_X_train, y_train)
stack_final = lr_final.predict(Stack_final_X_test)
print('최종 메타 모델의 예측 정확도 : ', accuracy_score(y_test, stack_final))
최종 메타 모델의 예측 정확도 : 0.9736842105263158
일반적으로 스태킹에 들어가는 모델들은 최적으로 파라미터를 튜닝한 상태에서 스태킹 모델을 만드는 것이 일반적!
회귀에서도 사용이 가능하다.
# 업로드용 창 맞추기
from IPython.core.display import display, HTML
display(HTML("<style>.container { width:90% !important; }</style>"))