○ permutation(순열) importance

- RFE와 같이 피처를 제거하면서 재학습을 수행할 필요가 없는 장점이 있음

- 사이킷런 feature selection 다른 모듈에 비해 일반적으로 많이 사용

- 테스트 데이터(검증 데이터)에 특정 피처의 값들을 반복적으로 무작위로 섞은 뒤 모델 성능이 얼마나 저하되는지를 기준으로 해당 피처의 중요도를 산정

permutation importance 프로세스

●모델 학습 후 원본 테스트 데이터로 기준 평가 성능을 설정

●원본 테스트(검증) 데이터의 개별 feature 별로 아래 수행

  • 설정된 iteration만큼 해당 feature 값들을 shuffle
  • 각각 모델 성능 평가 후 평균
  • 기준 평가 성능과 위에서 구한 평균을 비교해 모델 성능이 얼마나 저하되었는지 평가해서 피처 중요도 산정(중요한 피처면 저하가 큼) → 원본에서 평균적으로 얼마나 성능이 감소했나
# 실습을 위한 데이터 세트
from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split

diabetes = load_diabetes()
X_train, X_val, y_train, y_val = train_test_split(diabetes.data, diabetes.target, random_state=0)
# 모델 학습 및 R2 Score 평가(기준 평가 성능)
from sklearn.linear_model import Ridge # 사용할 모델
from sklearn.metrics import r2_score # 평가 척도

model = Ridge(alpha=1e-2).fit(X_train, y_train)
y_pred = model.predict(X_val)
print('r2 score:', r2_score(y_val, y_pred))

import numpy as np

# permutation_importance:원본 테스트(검증) 데이터 지정
from sklearn.inspection import permutation_importance

r = permutation_importance(model, X_val, y_val, n_repeats=30, random_state=0) # n_repeats:iteration 만큼 shuffle

for i in r.importances_mean.argsort()[::-1]: # 평균 permutation importance가 높은 순으로 인덱스를 내림차순 정렬
    if r.importances_mean[i] -2 * r.importances_std[i] > 0: # 표준편차의 2 배 값보다 큰 평균을 가진 피처들로 선별
        # 위 조건을 만족하는 피처들의 평균 permutation importance값과 표준편차 출력
        print(diabetes.feature_names[i], "   ", np.round(r.importances_mean[i], 4), ' +/-', np.round(r.importances_std[i], 5))

 - 표준편차의 2 배 값보다 큰 평균을 가진 피처들로 선별했는가는 단순히 경험적인 결과이다. 일반적으로 permutation importance 사용할 때 경험적으로 표준편차의 2 배 값 정도를 추천하지만 꼭 2배일 필요는 없다. 2~3배 사이, 예를 들어 2.5배, 2.8배, 3배 정도의 기준을 가지고 feature selection을 적용한 뒤 가장 성능이 뛰어난 결과를 적용해 보면 된다.

- permutation_importance를 적용해 버리면, 바로 피처 중요도가 permutation importance 방식으로 적용해버린다. 그러니까, permutation_importance.importances_mean 속성은 계산된 피처 중요도를 가지고 있다. 이중에서 높은 것부터 출력이 된다. 즉, 위의 결과에서 각 피처의 importance_mean의 값을 보니 s5가 0.2042로 모델 성능이 가장 저하해서 가장 중요한 피처이다.

 

 

Reference)

1. https://scikit-learn.org/stable/modules/permutation_importance.html

2. https://soohee410.github.io/iml_permutation_importance

3. https://blog.naver.com/passiona2z/222617818847

4. https://www.inflearn.com/questions/672420/permutation-importance-%EC%86%8C%EA%B0%9C-%EB%B0%8F-%EC%8B%A4%EC%8A%B5

5. https://www.inflearn.com/questions/483915/permutation-importance-%EC%88%9C%EC%84%9C%EA%B0%80-%EC%9E%98-%EC%9D%B4%ED%95%B4%EA%B0%80%EC%A7%80-%EC%95%8A%EC%8A%B5%EB%8B%88%EB%8B%A4


Feature Importance 문제점

왜 Feature Importance는 절대적인 Feature Selection 기준이 될 수 없는가?

○ Feature Importance는 최적 tree 구조를 만들기 위한 피처들의 impurity(불순도)가 중요 기준임. 결정값과 관련이 없어도 Feature Importance가 높아 질 수 있음

○ Feature Importance는 학습 데이터를 기반으로 생성됨. 테스트 데이터에서는 달라질 수 있음

○ Feature Importance는 number형의 높은 cardinality feature에 biased 되어 있음

Feature Selection

모델을 구성하는 주요 피처들을 선택

  • 불필요한 다수의 피처들로 인해 모델 성능을 떨어뜨릴 가능성 제거
  • 설명 가능한 모델이 될 수 있도록 피처들을 선별
  • 다수의 피처로 모델 학습시 과적합 이슈 발생(차원의 저주)

●Feature Selection 하는 방법

-피처값의 분포: 특정 피처의 값이 딱 하나라면, 이 피처가 있으나 없으나 피처와 타겟값 간의 패턴에 변별력이 없음

-결측값이 많은 피처: 결측값이 많은 피처는 제거 대상이 될 수 있음

-피처간 높은 상관도: '데이터 전처리-변수 분포 문제- 특징 간 상관성 제거' 참고

-결정값과의 독립성등을 고려: '데이터 전처리-차원의 저주 문제' 참고

-모델의 피처 중요도 기반: RFE, SelectFromModel, Permutation importance

 

 

 

○ RFE(Recursive Feature Elimination)

- 모델 최초 학습 후 feature 중요도 선정

- feature 중요도가 낮은 속성들을 차례로 제거해 가면서 반복적으로 학습/평가를 수행하여 최적 feature 추출

- 수행시간이 오래 걸리고, 낮은 속성들을 제거해 나가는 메커니즘이 정확한 feature selection을 찾는 목표에 정확히 부합하지 않을 수 있음

# 실습을 위한 임의의 분류용 데이터 생성
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=1000, n_features=25, n_informative=3,
                          n_redundant=2, n_repeated=0, n_classes=8,
                          n_clusters_per_class=1, random_state=0)

# 피처들을 데이터프레임 형태로 만들어보자
import pandas as pd

columns_list = ['col1', 'col2', 'col3', 'col4', 'col5', 'col6', 'col7', 'col8', 'col9', 'col10', 'col11', 'col12', 'col13',
               'col14', 'col15', 'col16', 'col17', 'col18', 'col19', 'col20', 'col21', 'col22', 'col23', 'col24', 'col25']
df_X = pd.DataFrame(X, columns=columns_list)
# RFE
from sklearn.feature_selection import RFE
from sklearn.svm import SVC # 사용할 모델

svc = SVC(kernel='linear')

rfe = RFE(estimator=svc, n_features_to_select=5, step=1) # step=1: 하나씩 피처 제거, n_features_to_select=5: 5개 피처를 선택
rfe.fit(X, y)
print(rfe.support_) # 선택된 피처는 True로 반환
print('선택된 피처:', df_X.columns[rfe.support_])

# 선택된 피처를 통한 최종 데이터프레임 생성
feature_selection = df_X.columns[rfe.support_]
df_X = df_X[feature_selection]
df = df_X # 데이터프레임 이름 변경
df['target'] = y

df.head()

○ RFECV(Recursive Feature Elimination with Cross Validation)

- RFECV는 RFE와 로직은 똑같지만, 각 feature 개수마다 교차검증을 활용해 각 feature 개수별 성능을 평균내어 가장 높은 성능을 가지는 feature 개수에 해당하는 feature들을 최종 feature selection 결과로 사용

→ 몇 개의 피처가 최적일지 알 수 있음

# 실습을 위한 임의의 분류용 데이터 생성
from sklearn.datasets import make_classification

X, y = make_classification(n_samples=1000, n_features=25, n_informative=3,
                          n_redundant=2, n_repeated=0, n_classes=8,
                          n_clusters_per_class=1, random_state=0)

# 피처들을 데이터프레임 형태로 만들어보자
import pandas as pd

columns_list = ['col1', 'col2', 'col3', 'col4', 'col5', 'col6', 'col7', 'col8', 'col9', 'col10', 'col11', 'col12', 'col13',
               'col14', 'col15', 'col16', 'col17', 'col18', 'col19', 'col20', 'col21', 'col22', 'col23', 'col24', 'col25']
df_X = pd.DataFrame(X, columns=columns_list)
# RFECV
from sklearn.feature_selection import RFECV
from sklearn.model_selection import StratifiedKFold
from sklearn.svm import SVC # 사용할 모델

svc = SVC(kernel='linear')

rfecv = RFECV(estimator=svc, step=1, cv=StratifiedKFold(3), scoring='accuracy')
rfecv.fit(X, y)
# 몇 개의 피처가 최적일지 교차검증 성능 시각화
import matplotlib.pyplot as plt

plt.figure()
plt.xlabel('Number of features selected')
plt.ylabel('Cross validation score')
plt.plot(range(1, len(rfecv.grid_scores_) + 1), rfecv.grid_scores_)
plt.show() # cv를 3으로 해서 3개의 그래프가 나옴
# 0~5 사이에 특정 값이 최적의 피처 개수인듯

print('최적 피처 개수:', rfecv.n_features_)
print('선택된 피처:', df_X.columns[rfecv.support_])

# 선택된 피처를 통한 최종 데이터프레임 생성
feature_selection = df_X.columns[rfecv.support_]
df_X = df_X[feature_selection]
df = df_X # 데이터프레임 이름 변경
df['target'] = y

df.head()

○ SelectFromModel

- 모델 최초 학습 후 선정된 feature 중요도에 따라 평균/중앙값 등의 특정 비율 이상인 feature들을 선택

(이 비율은 threshold로 지정)

# 실습을 위한 데이터 세트
from sklearn.datasets import load_diabetes

diabetes = load_diabetes()
X = diabetes.data
y = diabetes.target
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LassoCV # 사용할 모델

lasso = LassoCV()
lasso.fit(X,y) # 모델 학습

# 회귀계수 절대값 시각화
importance = np.abs(lasso.coef_)
plt.bar(height=importance, x=diabetes.feature_names)
plt.title('feature importances via coefficients')
plt.show()

# selectfrommodel
from sklearn.feature_selection import SelectFromModel

threshold = np.sort(importance)[-3] + 0.01 # 회귀계수가 3번째로 큰 값에 0.01을 더해서 threshold 지정하기로 결정
sfm = SelectFromModel(lasso, threshold=threshold)
sfm.fit(X,y)

print(sfm.threshold_) # threshold 값
print(sfm.get_support()) # 선택된 피처는 True로 반환
print('선택된 피처:', np.array(diabetes.feature_names)[sfm.get_support()])

# 다른 threshold 지정해보기
sfm = SelectFromModel(lasso, threshold='1.5 * median')
sfm.fit(X, y)

print(sfm.threshold_)
print(sfm.get_support())
print('선택된 피처:', np.array(diabetes.feature_names)[sfm.get_support()])

SelectFromModel Reference)

https://bizzengine.tistory.com/163

 

Automatic Feature Selection

개요 새로운 특성을 만드는 방법이 많으므로, 데이터의 차원이 원복 특성의 수 이상으로 증가하기 쉽습니다. 그러나, 특성이 더 추가되면 모델은 더 복잡해지고 과대적합될 가능성도 높아집니

bizzengine.tistory.com


사이킷런의 Recursive Feature Elimination이나 SelectFromModel의 경우 트리기반의 분류에서는 Feature importance, 회귀에서는 회귀 계수를 기반으로 반복적으로 모델 평가하면서 피처들을 선택하는 방식인데, 이런 방식의 Feature Selection은 오히려 모델의 성능을 떨어뜨릴 가능성이 높다. 그러니까 정확하지 않은 feature selection 방법이다.

 Permutation importance가 보다 정확한 방식

 

이어서 permutation importance 정리 및 실습 진행

 

Reference)

https://www.inflearn.com/questions/806218/feature-selection

스태킹(Stacking)

개별 알고리즘의 예측 결과 데이터 세트를 최종적인 메타 데이터 세트로 만들어 별도의 ML 알고리즘으로 최종 학습을 수행하고 테스트 데이터를 기반으로 다시 최종 예측을 수행하는 방식

→기반 모델들이 예측한 값들을 Stacking 형태로 만들어서 메타 모델이 이를 학습하고 예측하는 모델

 

●스태킹 모델은 다음과 같은 두 종류의 모델이 필요함

  1. 개별적인 기반 모델
  2. 이 개별 기반 모델의 예측 데이터를 학습 데이터로 만들어서 학습하는 최종 메타 모델

즉, 스태킹 모델은 여러 개별 모델의 예측 데이터를 각각 스태킹 형태로 결합해 최종 메타 모델의 학습용 피처 데이터 세트와 테스트용 피처 데이터 세트를 만드는 것

 

기본 스태킹

# 필요한 모듈 불러오기
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()
# 개별 ML 모델 학습
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)
# 개별 ML 모델 예측
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)

# 개별 ML 모델의 정확도 확인
print('KNN 정확도:{0:.4f}'.format(accuracy_score(y_test, knn_pred)))
print('랜덤 포레스트 정확도:{0:.4f}'.format(accuracy_score(y_test, rf_pred)))
print('결정트리 정확도:{0:.4f}'.format(accuracy_score(y_test, dt_pred)))
print('에이다부스트 정확도:{0:.4f}'.format(accuracy_score(y_test, ada_pred)))

# 개별 ML 모델의 예측 결과를 피처로 만듬
pred = np.array([knn_pred, rf_pred, dt_pred, ada_pred]) # 예측 결과를 행 형태로 붙임 
print(pred.shape)

pred = np.transpose(pred) # transpose를 이용해 행과 열의 위치 교환. 컬럼별로 각 알고리즘의 예측 결과를 피처로 만듬
print(pred.shape)

# 최종 메타 모델인 로지스틱 회귀 학습/예측/평가
lr_final.fit(pred, y_test)
final = lr_final.predict(pred)

print('최종 메타 모델의 예측 정확도:{0:.4f}'.format(accuracy_score(y_test, final)))

메타 모델인 로지스틱 회귀 기반에서 최종 학습할 때 레이블 데이터 세트로 학습 데이터가 아닌 테스트용 레이블 데이터 세트를 기반으로 학습했기에 과적합 문제가 존재 → 과적합을 개선하기 위한 'CV 세트 기반의 스태킹'

 

CV 세트 기반의 스태킹

개별 모델들이 각각 교차검증으로 메타 모델을 위한 학습용 스태킹 데이터 생성과 예측을 위한 테스트용 스태킹 데이터를 생성한 뒤 이를 기반으로 메타 모델이 학습과 예측을 수행한다. 

 

이해를 돕기 위해 다음과 같이 2단계의 스텝으로 구분하자.

Step1: 각 모델별로 원본 학습/테스트 데이터를 예측한 결과값을 기반으로 메타 모델을 위한 학습용/테스트용 데이터를 생성

Step2: Step1에서 개별 모델들이 생성한 학습용 데이터를 모두 스태킹 형태로 합쳐서 메타 모델이 학습할 최종 학습용 데이터 세트를 생성함. 마찬가지로 각 모델들이 생성한 테스트용 데이터를 모두 스태킹 형태로 합쳐서 메타 모델이 예측할 최종 테스트 데이터 세트를 생성함. 메타 모델은 최종적으로 생성된 학습 데이터 세트와 원본 학습 데이터의 레이블 데이터를 기반으로 학습한 뒤, 최종적으로 생성된 테스트 데이터 세트를 예측하고, 원본 테스트 데이터의 레이블 데이터를 기반으로 평가함

 

Step1의 과정을 도식화해서 로직을 알아보자.

 

●Step1

from sklearn.model_selection import KFold

# 개별 기반 모델에서 최종 메타 모델이 사용할 학습 및 테스트용 데이터를 생성하기 위한 함수 생성
def get_stacking_base_datasets(model, X_train_n, y_train_n, X_test_n, n_folds):
    kf = KFold(n_splits=n_folds, shuffle=False) # ,random_state=0)   # Default shuffle=False 
    # 추후에 메타 모델이 사용할 학습 데이터 반환을 위한 넘파이 배열 초기화
    train_fold_pred = np.zeros((X_train_n.shape[0], 1)) # 2차원, np.zeros((a,b))
    test_pred = np.zeros((X_test_n.shape[0], n_folds)) # 2차원
    print(model.__class__.__name__, 'model 시작')
    
    for folder_counter, (train_index, valid_index) in enumerate(kf.split(X_train_n)):
        # 입력된 학습 데이터에서 기반 모델이 학습/예측할 폴드 데이터 세트 추출
        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) # 2차원, reshape로 1차원에서 2차원으로 변경
        # 입력된 원본 테스트 데이터를 폴드 세트내 학습된 기반 모델에서 예측 후 데이터 저장
        test_pred[:, folder_counter] = model.predict(X_test_n)
        
    # 폴드 세트 내에서 원본 테스트 데이터를 예측한 데이터를 평균하여 테스트 데이터로 생성
    test_pred_mean = np.mean(test_pred, axis=1).reshape(-1, 1) # 2차원, reshape로 1차원에서 2차원으로 변경
    
    # train_fold_pred는 최종 메타 모델이 사용하는 학습 데이터, test_pred_mean은 테스트 데이터
    return train_fold_pred, test_pred_mean
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)

 

●Step2

Stack_final_X_train = np.concatenate((knn_train, rf_train, dt_train, ada_train), axis=1)
Stack_final_X_test = np.concatenate((knn_test, rf_test, dt_test, ada_test), axis=1)
print('원본 학습 피처 데이터 shape:', X_train.shape, '원본 테스트 피처 shape:', X_test.shape)
print('스태킹 학습 피처 데이터 shape:', Stack_final_X_train.shape,
     '스태킹 테스트 피처 데이터 shape:', Stack_final_X_test.shape)

lr_final.fit(Stack_final_X_train, y_train)
stack_final = lr_final.predict(Stack_final_X_test)

print('최종 메타 모델의 예측 정확도:{0:.4f}'.format(accuracy_score(y_test, stack_final)))

 

스태킹 모델은 다른 앙상블 모델과 마찬가지로 분류뿐만 아니라 회귀에도 적용 가능하다.

 

위의 실습에서는 개별 모델의 알고리즘에서 파라미터 튜닝을 최적으로 하지 않았지만,

일반적으로 스태킹 모델의 파라미터 튜닝은 개별 알고리즘 모델의 파라미터를 최적으로 튜닝하는 것을 말한다. 즉 스태킹을 이루는 모델은 최적으로 파라미터를 튜닝한 상태에서 스태킹 모델을 만드는 것이 일반적이다.

●해당 데이터 세트는 불균형 데이터 세트로, 레이블인 Class 속성은 매우 불균형한 분포를 가짐

Class 0 → 정상적인 신용카드 트랜잭션 데이터

Class 1 → 신용카드 사기 트랜잭션

●전체 데이터의 약 0.172%만이 레이블 값이 1, 즉 사기 트랜잭션임

●일반적으로 사기 검출, 이상 검출과 같은 데이터 세트는 이처럼 레이블 값이 극도로 불균형한 분포를 가지기 쉬움. 왜냐하면 사기와 같은 이상 현상은 전체 데이터에서 차지하는 비중이 매우 적을 수밖에 없기 때문


1. Data load

●필요한 모듈 및 패키지 불러오기

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(r'C:\Users\82102\PerfectGuide\4장\creditcard.csv')
card_df.head()

card_df.info()
card_df.isnull().sum()
card_df.describe()

●데이터 불균형 확인

card_df['Class'].value_counts()

  • Time 피처의 경우 데이터 생성 관련한 작업용 속성으로서 큰 의미가 없기에 제거
  • Amount 피처는 신용카드 트랜잭션 금액
  • Class는 레이블로서 0의 경우 정상, 1의 경우 사기 트랜잭션
  • 결측값은 없음
  • Class는 int형, 나머지 피처들은 float형

2. 데이터 1차 가공 및 모델 학습/예측/평가

●Time 피처 삭제

# 인자로 입력받은 DataFrame을 복사한 뒤 Time 피처만 삭제하고 복사된 DataFrame 반환하는 함수 생성
def get_preprocessed_df(df=None):
    df_copy = df.copy()
    df_copy.drop('Time', axis=1, inplace=True)
    return df_copy

●학습 / 테스트 데이터 세트 분리

  • stratify 방식 사용: 불균형 데이터 세트이므로 학습 / 테스트 데이터 세트는 원본 데이터와 같은 레이블 값 분포로 맞춰주고 → 학습 / 테스트 데이터 세트의 레이블 값 분포를 동일하게 설정
from sklearn.model_selection import train_test_split

# 사전 데이터 가공 후 학습과 테스트 데이터 세트를 반환하는 함수 생성
def get_train_test_dataset(df=None):
    # Time 피처를 삭제한 df를 받아옴
    df_copy = get_preprocessed_df(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('학습 데이터 레이블 값 비율')
print(y_train.value_counts() / y_train.shape[0] * 100)
print('\n')
print('테스트 데이터 레이블 값 비율')
print(y_test.value_counts() / y_test.shape[0] * 100)

●모델 성능 평가 함수 선언

from sklearn.metrics import *

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))

●모델 학습/예측/평가

1) LogisticRegression

from sklearn.linear_model import LogisticRegression

lr_clf = LogisticRegression(max_iter=1000)
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)

  • 매번, 모델 학습/예측/평가 하는 코드 작성이 귀찮으므로 반복적으로 모델을 변경해 학습/예측/평가 하는 함수 생성
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)

2) LightGBM

LightGBM 2.1.0 이상의 버전에서 boost_from_average 파라미터의 default가 False에서 True로 변경됨. 레이블 값이 극도로 불균형한 분포를 이루는 경우 boost_from_average=True 설정은 재현율 및 ROC-AUC 성능을 매우 크게 저하시킴. 따라서 레이블 값이 극도로 불균형할 경우 boost_from_average를 False로 설정하는 것이 유리

# 본 데이터 세트는 극도로 불균형한 레이블 값 분포도를 가지므로 boost_from_average=False로 설정
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)

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

왜곡된 분포도를 가지는 데이터를 재가공한 뒤에 모델을 다시 테스트 해보자

 

●Amount 피처 분포도 확인

Amount 피처는 신용 카드 사용 금액으로 정상/사기 트랜잭션을 결정하는 매우 중요한 피처일 가능성이 높다.

import seaborn as sns

plt.figure(figsize=(8,4))
plt.xticks(range(0, 30000, 1000), rotation=60)
sns.histplot(card_df['Amount'], bins=100, kde=True)
plt.show()

Amount 피처는 1000 이하인 데이터가 대부분이며, 26000까지 드물지만 많은 금액을 사용한 경우가 발생하면서 꼬리가 긴 형태의 분포 곡선을 가진다

 

대부분의 선형 모델은 중요 피처들의 값이 정규분포 형태를 유지하는 것을 선호하기 때문에 Amount 피처를 표준 정규분포 형태로 변환하자

 

●standardscaler를 이용해 Amount 피처 변환

from sklearn.preprocessing import StandardScaler

def get_preprocessed_df(df=None):
    df_copy = df.copy()
    
    # Amount 피처를 표준 정규분포 형태로 변환
    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를 표준 정규분포 형태로 변환 후 모델 학습/예측/평가
X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression(max_iter=1000)
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

print('### LightGBM 예측 성능 ###')
lgmb_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()
    
    # Amount 피처를 로그변환
    amount_n = np.log1p(df_copy['Amount'])
    
    # 변환된 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를 로그변환 후 모델 학습/예측/평가
X_train, X_test, y_train, y_test = get_train_test_dataset(card_df)

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression(max_iter=1000)
get_model_train_eval(lr_clf, ftr_train=X_train, ftr_test=X_test, tgt_train=y_train, tgt_test=y_test)

print('### LightGBM 예측 성능 ###')
lgmb_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)

4. 이상치 데이터 제거 후 모델 학습/예측/평가

●IQR을 이용해 이상치 데이터 제거

  • 모든 피처들의 이상치를 검출하는 것은 시간이 많이 소모되며, 결정값과 상관성이 높지 않은 피처들의 경우는 이상치를 제거하더라도 크게 성능 향상에 기여하지 않기 때문에 매우 많은 피처가 있을 경우 이들 중 결정값과 가장 상관성이 높은 피처들을 위주로 이상치를 검출하는 것이 좋다.
import seaborn as sns

plt.figure(figsize=(9,9))
corr = card_df.corr() # DataFrame의 각 피처별로 상관관계를 구함
sns.heatmap(corr, cmap='RdBu') # 상관관계를 시본의 heatmap으로 시각화

상관관계 히트맵에서 Class와 음의 상관관계가 가장 높은 V14, V17 중 V14에 대해서만 이상치를 찾아서 제거해보자

# 이상치 데이터 검출
import numpy as np

def get_outlier(df=None, column=None, weight=1.5):
    quantile_25 = np.percentile(df[column].values, 25)
    quantile_75 = np.percentile(df[column].values, 75)
    
    iqr = quantile_75 - quantile_25
    iqr_weight = iqr * weight
    
    lowest = quantile_25 - iqr_weight
    highest = quantile_75 + iqr_weight
    
    # 최소와 최대 사이에 있지 않은 값은 이상치로 간주하고 인덱스 반환
    outlier_index = df[column][(df[column] < lowest) | (df[column] > highest)].index
    
    return outlier_index
outlier_index = get_outlier(df=card_df, column='V14', weight=1.5)
print('이상치 데이터 인덱스:', outlier_index)
print('이상치 데이터 개수:', len(outlier_index))

  • 현재 creditcard 데이터는 전체 데이터를 기반으로 IQR을 설정하면 너무 많은 데이터가 이상치로 설정되서 삭제되는 범위가 너무 커짐
  • 일반적으로 이상치가 이렇게 많다는 것은 말이 안된다고 판단, 그리고 이상치 제거는 가능한 최소치로 해줘야함
  • 전체 데이터를 IQR로 하면 실제 사기 데이터에 잘 동작하는 데이터도 삭제 시켜버리고 있음

→creditcard 데이터(레이블이 불균형한 분포를 가진 불균형한 데이터)를 통해 생성한 모델은 실제값 0을 0으로 예측하는 것은 기본적으로 잘됨. 하지만 카드 사기 검출 모델에서 중요한 것은 실제 사기 즉 1을 1로 예측하는 것이 중요하므로 타겟값이 1인 데이터에 대해서 적절한 이상치를 제거하는 것이 필요함. 때문에 현재 데이터에서는 1의 데이터만 일정 수준에서 이상치를 제거해 주는게 좋음

# 타겟값이 1인 데이터에 대해서 이상치 데이터 검출
import numpy as np

def get_outlier(df=None, column=None, weight=1.5):
    fraud = df[df['Class']==1][column]
    
    quantile_25 = np.percentile(fraud, 25)
    quantile_75 = np.percentile(fraud, 75)
    
    iqr = quantile_75 - quantile_25
    iqr_weight = iqr * weight
    
    lowest = quantile_25 - iqr_weight
    highest = quantile_75 + iqr_weight
    
    # 최소와 최대 사이에 있지 않은 값은 이상치로 간주하고 인덱스 반환
    outlier_index = fraud[(fraud < lowest) | (fraud > highest)].index
    
    return outlier_index
outlier_index = get_outlier(df=card_df, column='V14', weight=1.5)
print('이상치 데이터 인덱스:', outlier_index)

# 이상치 데이터 제거
def get_preprocessed_df(df=None):
    df_copy = df.copy()
    
    # Amount 피처를 로그변환
    amount_n = np.log1p(df_copy['Amount'])
    
    # 변환된 Amount 피처를 Amount_Scaled로 피처명 변경후 DataFrame 맨 앞 컬럼으로 추가
    df_copy.insert(0, 'Amount_Scaled', amount_n)
    
    # 기존 Time, Amount 피처 삭제
    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('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression(max_iter=1000)
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)

5. 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) # X_train, y_train은 4번 과정까지 전처리 된 후 split된 데이터

# 데이터 확인
print('SMOTE 적용 전 학습용 데이터 세트:', X_train.shape, y_train.shape)
print('SMOTE 적용 후 학습용 데이터 세트:', X_train_over.shape, y_train_over.shape)
print('SMOTE 적용 후 레이블 값 분포')
print(pd.Series(y_train_over).value_counts())

●모델 학습/예측/평가

print('### 로지스틱 회귀 예측 성능 ###')
lr_clf = LogisticRegression(max_iter=1000)
get_model_train_eval(lr_clf, ftr_train=X_train_over, ftr_test=X_test, tgt_train=y_train_over, 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_over, ftr_test=X_test, tgt_train=y_train_over, tgt_test=y_test)

로지스틱 회귀는 SMOTE 적용 후 재현율이 92.47%로 크게 증가했지만 정밀도가 5.4%로 급격하게 감소했음. 재현율이 높더라도 이 정도로 저조한 정밀도로는 현실 업무에 적용할 수 없음.

로지스틱 회귀 모델에 어떠한 문제가 발생하고 있는지 분류 결정 임계값(threshold)에 따른 정밀도와 재현율 곡선을 통해 알아보자.

precisions, recalls, thresholds = precision_recall_curve(y_test, lr_clf.predict_proba(X_test)[:, 1])
    
# 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축 labe과 legend, grid 설정
plt.xlabel('Threshold value')
plt.ylabel('Precision and Recall value')
plt.legend()
plt.grid()
plt.show()


(번외로, LightGBM 모델도 precision_recall_curve를 그리려면 다음과 같이 함수를 만드는 것이 편리)

from sklearn.metrics import precision_recall_curve

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축 labe과 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])
precision_recall_curve_plot(y_test, lgbm_clf.predict_proba(X_test)[:, 1])

threshold가 0.99 이하에서는 재현율이 매우 좋고, 정밀도가 극단적으로 낮다가 0.99 이상에서는 반대로 재현율이 대폭 떨어지고 정밀도가 높아짐. threshold를 조정하더라도 threshold의 민감도가 너무 심해 올바른 재현율/정밀도 성능을 얻을 수 없으므로 로지스틱 회귀 모델의 경우 SMOTE 적용 후 올바른 예측 모델이 생성되지 못했음.

 

LightGBM의 모델 성능 결과를 보면, 재현율은 증가했지만 정밀도가 감소했음을 알 수 있음. 이처럼 SMOTE를 적용하면 재현율은 높아지나, 정밀도는 낮아지는 것이 일반적임. 때문에 정밀도 지표보다 재현율 지표를 높이는 것이 중요한 업무에서SMOTE를 사용하면 좋음!

log 변환

'데이터 전처리-머신러닝 모델의 성능 향상을 위한 전처리-변수 분포 문제-변수 치우침 제거' 참고

'파이썬 머신러닝 완벽가이드/[2장] 사이킷런으로 시작하는 머신러닝/스케일링, 로그변환' 참고

 

log변환은 왜곡된 분포를 가진 피처를 비교적 정규분포에 가깝게 변환

 

IQR를 이용한 이상치 제거

'데이터 전처리-머신러닝 모델의 성능 향상을 위한 전처리-변수 분포 문제-이상치 제거' 참고

 

 

언더 샘플링과 오버 샘플링

'데이터 전처리-머신러닝 모델의 성능 향상을 위한 전처리-클래스 불균형 문제, 클래스 불균형 문제 해결방법' 참고

 

●레이블이 불균형한 분포를 가진 데이터 세트를 학습 시, 이상 레이블을 가지는 데이터 건수가 정상 레이블을 가진 데이터 건수에 비해 너무 적어 제대로 된 유형의 학습이 어려움. 즉, 이상 레이블을 가지는 데이터 건수는 매우 적기 때문에 제대로 다양한 유형을 학습하지 못하는 반면에 정상 레이블을 가지는 데이터 건수는 매우 많기 때문에 일방적으로 정상 레이블로 치우친 학습을 수행해 제대로 된 이상 데이터 검출이 어려움

●지도학습 분류에서 불균형한 레이블 값 분포로 인한 문제를 해결하는 대표적인 방법은 오버 샘플링과 언더 샘플링이 있음

 

캐글의 산탄데르 고객 만족 데이터 세트에 대해서 고객 만족 여부를 XGBoost와 LightGBM을 활용해 예측한다.

데이터 세트에서 feature는 370개로 구성되며, 클래스 레이블 명은 TARGET이다. 

TARGET=1 → 불만 고객

TARGET=0 → 만족 고객

 

이 데이터 세트는 불균형 데이터 세트이므로 모델의 성능 평가는 정확도가 아닌 roc_auc가 더 적합하다.


1. Data load and EDA

●필요한 모듈 및 패키지 불러오기

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib
import warnings
warnings.filterwarnings('ignore')

●데이터 로드

cust_df = pd.read_csv(r"C:\Users\82102\PerfectGuide\4장\train_santander.csv", encoding='latin-1')
cust_df.head()
cust_df.shape

cust_df.info()

cust_df.isnull().sum()

 

sum(cust_df.isnull().sum()==1)

  • 370개의 feature, 1개의 class 컬럼으로 구성
  • 111개의 float형 feature, 260개의 int형 feature로 모든 feature가 숫자형
  • 결측값은 없음

●클래스 비율 확인

cust_df['TARGET'].value_counts()

print('만족 비율:', cust_df['TARGET'].value_counts()[0] / cust_df.shape[0])
print('불만족 비율:', cust_df['TARGET'].value_counts()[1] / cust_df.shape[0])

  • 대부분이 만족이며 불만족인 고객은 약 4%에 불과 → 불균형 데이터 세트

●각 피처의 분포 확인

cust_df.describe()

var3 컬럼의 경우 min 값이 -999999이다. 이 값은 NaN이나 특정 예외 값을 -999999로 변환한 것으로 보인다.

2. Preprocessing

●위에서 문제가 있던 var3 컬럼의 값 분포 확인

cust_df['var3'].value_counts()

-999999 값이 116개가 존재하고 다른 값에 비해서 편차도 심함 → 가장 값이 많은 2로 변환하기로 결정

●var3 컬럼 -999999 값을 가장 값이 많은 2로 변환

cust_df['var3'].replace(-999999, 2, inplace=True)

●ID 피처는 단순 식별자에 불과하므로 드롭

cust_df.drop('ID', axis=1, inplace=True)

●피처와 클래스 분리

X_features = cust_df.iloc[:, :-1]
y_labels = cust_df.iloc[:, -1]

●학습 데이터 세트와 테스트 데이터 세트로 분리(불균형 데이터 세트이므로 stratify를 이용해 학습 데이터 세트와 테스트 데이터 세트의 클래스 비율을 원본 데이터의 클래스 비율과 유사하게 맞춰주기로 결정)

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X_features, y_labels, test_size=0.2, stratify=y_labels, random_state=0)
# 학습 데이터, 테스트 데이터 target 값 분포 확인
print('학습 데이터 세트 target 값 분포')
print(y_train.value_counts() / y_train.shape[0])
print('\n')
print('테스트 데이터 세트 target 값 분포')
print(y_test.value_counts() / y_test.shape[0])

  • 학습과 테스트 데이터 세트 target의 분포가 비슷하게 추출됐고, 이 분포는 원본 데이터와 유사하게 추출됨

● XGBoost, LightGBM의 early stopping을 사용하기 위해 X_train, y_train을 다시 학습과 검증 데이터 세트로 분리

(마찬가지로 stratify를 이용하기로 결정)

# X_train, y_train을 다시 학습과 검증 데이터 세트로 분리
X_tr, X_val, y_tr, y_val = train_test_split(X_train, y_train, test_size=0.3, stratify=y_train, random_state=0)

3. XGBoost Model

●대략적인 모델의 성능을 확인해보자

from xgboost import XGBClassifier
from sklearn.metrics import roc_auc_score

# n_estimators=500, learning_rate=0.05, random_state는 수행 시마다 동일 예측 결과를 위해 설정
xgb_clf = XGBClassifier(n_estimators=500, learning_rate=0.05, random_state=156)

# 성능 평가 기준이 roc_auc이므로 eval_metric은 'auc'로 설정, 조기 중단은 100으로 설정하고 학습 수행
xgb_clf.fit(X_tr, y_tr, early_stopping_rounds=100, eval_metric='auc', eval_set=[(X_tr, y_tr), (X_val, y_val)])

xgb_roc_score = roc_auc_score(y_test, xgb_clf.predict_proba(X_test)[:, 1])
print('ROC AUC:{0:.4f}'.format(xgb_roc_score))

●HyperOpt를 이용해 XGBoost의 하이퍼 파라미터 튜닝

# 1. 검색 공간 설정
from hyperopt import hp

xgb_search_space = {'max_depth':hp.quniform('max_depth', 5, 15, 1), # 정수형 하이퍼 파라미터 → hp.quniform() 사용
                   'min_child_weight':hp.quniform('min_child_weight', 1, 6, 1), # 정수형 하이퍼 파라미터 → hp.quniform() 사용
                   'colsample_bytree':hp.uniform('colsample_bytree', 0.5, 0.95),
                   'learning_rate':hp.uniform('learning_rate', 0.01, 0.2)}
  • 교차검증 시 XGBoost, LightGBM의 조기 중단(early stopping)과 검증 데이터 성능 평가를 위해서 KFold를 이용해 직접 학습과 검증 데이터 세트를 추출하고 이를 교차검증 횟수만큼 학습과 성능 평가를 수행한다.(이전 글에서 말했듯이, XGBoost와 LightGBM은 교차검증 시 cross_val_score()를 사용하면 조기 중단이 지원되지 않음)
  • 수행 시간을 줄이기 위해 n_estimators는 100으로 줄이고, early_stopping_rounds도 30으로 줄여서 테스트한 뒤 나중에 하이퍼 파라미터 튜닝이 완료되면 다시 증가시켜 모델 학습 및 평가 진행하기로 함(수행 시간 때문에)
# 2. 목적 함수 설정
from sklearn.model_selection import KFold # 교차검증
from sklearn.metrics import roc_auc_score
## from hyperopt import STATUS_OK

def objective_func(search_space):
    xgb_clf = XGBClassifier(n_estimators=100,
                            max_depth=int(search_space['max_depth']), # 정수형 하이퍼 파라미터 형변환 필요:int형
                            min_child_weight=int(search_space['min_child_weight']), # 정수형 하이퍼 파라미터 형변환 필요:int형
                            colsample_bytree=search_space['colsample_bytree'],
                            learning_rate=search_space['learning_rate'])
    
    # 3개 k-fold 방식으로 평가된 roc_auc 지표를 담는 list
    roc_auc_list = []
    
    # 3개 k-fold 방식 적용
    kf = KFold(n_splits=3)
    # X_train을 다시 학습과 검증용 데이터로 분리
    for tr_index, val_index in kf.split(X_train):
        # kf.split(X_train)으로 추출된 학습과 검증 index 값으로 학습과 검증 데이터 세트 분리
        X_tr, y_tr = X_train.iloc[tr_index], y_train.iloc[tr_index]
        X_val, y_val = X_train.iloc[val_index], y_train.iloc[val_index]
        
        # early stopping은 30회로 설정하고 추출된 학습과 검증 데이터로 XGBClassifier 학습 수행
        xgb_clf.fit(X_tr, y_tr, early_stopping_rounds=30, eval_metric='auc', eval_set=[(X_tr, y_tr), (X_val, y_val)])
        
        # 1로 예측한 확률값 추출 후 roc auc 계산하고, 평균 roc auc 계산을 위해 list에 결과값 담음
        score = roc_auc_score(y_val, xgb_clf.predict_proba(X_val)[:, 1])
        roc_auc_list.append(score)
        
    # 3개 k-fold로 계산된 roc_auc 값의 평균값을 반환하되 -1을 곱함
    return -1 * np.mean(roc_auc_list) # return {'loss':-1 * np.mean(roc_auc_list), 'status':STATUS_OK}
# 3. fmin()을 이용해 최적 하이퍼 파라미터 도출
from hyperopt import fmin, tpe, Trials

trials = Trials()

best = fmin(fn=objective_func,
           space=xgb_search_space,
           algo=tpe.suggest,
           max_evals=50,
           trials=trials,
           # rstate=np.random.default_rng(seed=30)
           )

print('best:', best)

추출된 최적 하이퍼 파라미터를 이용하여 XGBoost의 인자로 입력하여 학습 및 평가

  • n_estimators=500, early_stopping_rounds=100으로 증가시켜서 학습 및 평가 진행
# 도출된 최적 하이퍼 파라미터를 이용하여 모델 선언
xgb_clf = XGBClassifier(n_estimators=500,
                       learning_rate=round(best['learning_rate'], 5),
                        max_depth=int(best['max_depth']),
                        min_child_weight=int(best['min_child_weight']),
                        colsample_bytree=round(best['colsample_bytree'], 5)
                       )
# 모델 학습
xgb_clf.fit(X_tr, y_tr, early_stopping_rounds=100,
           eval_metric='auc', eval_set=[(X_tr, y_tr), (X_val, y_val)])

# 예측 및 모델 평가
xgb_roc_score = roc_auc_score(y_test, xgb_clf.predict_proba(X_test)[:, 1])
print('ROC AUC:{0:.4f}'.format(xgb_roc_score))

●피처 중요도

from xgboost import plot_importance
import matplotlib.pyplot as plt
%matplotlib inline

fig, ax = plt.subplots(figsize=(10,8))
plot_importance(xgb_clf, ax=ax, max_num_features=20, height=0.4) # 상위 20개 피처

4. LightGBM Model

●대략적인 모델의 성능을 확인해보자

from lightgbm import LGBMClassifier

lgbm_clf = LGBMClassifier(n_estimators=500)

eval_set = [(X_tr, y_tr), (X_val, y_val)]
lgbm_clf.fit(X_tr, y_tr, early_stopping_rounds=100, eval_metric='auc', eval_set=eval_set)

lgbm_roc_score = roc_auc_score(y_test, lgbm_clf.predict_proba(X_test)[:, 1])
print('ROC AUC:{0:.4f}'.format(lgbm_roc_score))

●HyperOpt를 이용해 LightGBM의 하이퍼 파라미터 튜닝

# 1. 검색 공간 설정
lgbm_search_space = {'num_leaves':hp.quniform('num_leaves', 32, 64, 1),
                    'max_depth':hp.quniform('max_depth', 100, 160, 1),
                    'min_child_samples':hp.quniform('min_child_samples', 60, 100, 1),
                    'subsample':hp.uniform('subsample', 0.7, 1),
                    'learning_rate':hp.uniform('learning_rate', 0.01, 0.2)
                    }
# 2. 목적 함수 설정
def objective_func(search_space):
    lgbm_clf = LGBMClassifier(n_estimators=100,
                             num_leaves=int(search_space['num_leaves']),
                              max_depth=int(search_space['max_depth']),
                              min_child_samples=int(search_space['min_child_samples']),
                              subsample=search_space['subsample'],
                              learning_rate=search_space['learning_rate'])
    
    roc_auc_list=[]
    
    kf = KFold(n_splits=3)
    for tr_index, val_index in kf.split(X_train):
        X_tr, y_tr = X_train.iloc[tr_index], y_train.iloc[tr_index]
        X_val, y_val = X_train.iloc[val_index], y_train.iloc[val_index]
        
        lgbm_clf.fit(X_tr, y_tr, early_stopping_rounds=30, eval_metric='auc',
                    eval_set=[(X_tr, y_tr), (X_val, y_val)])
        
        score = roc_auc_score(y_val, lgbm_clf.predict_proba(X_val)[:, 1])
        roc_auc_list.append(score)
        
    return -1 * np.mean(roc_auc_list)
# 3. fmin()을 이용해 최적 하이퍼 파라미터 도출
from hyperopt import fmin, tpe, Trials

trials = Trials()

best = fmin(fn=objective_func,
           space=lgbm_search_space,
           algo=tpe.suggest,
           max_evals=50,
           trials=trials,
           # rstate=np.random.default_rng(seed=30)
           )

print('best:', best)

추출된 최적 하이퍼 파라미터를 이용하여 LightGBM의 인자로 입력하여 학습 및 평가

lgbm_clf = LGBMClassifier(n_estimators=500,
                         num_leaves=int(best['num_leaves']),
                          max_depth=int(best['max_depth']),
                          min_child_samples=int(best['min_child_samples']),
                          subsample=round(best['subsample'], 5),
                          learning_rate=round(best['learning_rate'], 5)
                         )

lgbm_clf.fit(X_tr, y_tr, early_stopping_rounds=100, eval_metric='auc',
            eval_set=[(X_tr, y_tr), (X_val, y_val)])

lgbm_roc_score = roc_auc_score(y_test, lgbm_clf.predict_proba(X_test)[:, 1])
print('ROC AUC:{0:.4f}'.format(lgbm_roc_score))

 

HyperOpt를 이용한 XGBoost 하이퍼 파라미터 튜닝

HyperOpt를 이용하여 XGBoost 하이퍼 파라미터를 최적화 해본다. 주의사항은 다음과 같다.

  • HyperOpt는 입력값과 반환값이 모두 실수형이기 때문에 정수형 하이퍼 파라미터 입력 시 형변환 필요
  • HyperOpt의 목적 함수는 최소값을 반환할 수 있도록 최적화하기 때문에 성능 값이 클수록 좋은 성능 지표일 경우 -1을 곱해주어야 함(분류는 성능 값이 클수록 좋기때문에 -1을 곱해주고, 회귀는 성능 값이 작을수록 좋기 때문에 -1을 곱하지 않고 그대로 사용) 

○필요한 패키지 및 모듈 불러오기

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

○데이터 로드

# 유방암 데이터셋 
dataset = load_breast_cancer()

cancer_df = pd.DataFrame(data=dataset.data, columns=dataset.feature_names)
cancer_df['target'] = dataset.target
cancer_df.head()

○데이터 분리

# 피처와 타겟 분리
X_features = cancer_df.iloc[:, :-1]
y_label = cancer_df.iloc[:, -1]
# 전체 데이터 중 80%는 학습용 데이터, 20%는 테스트용 데이터 분리
X_train, X_test, y_train, y_test = train_test_split(X_features, y_label, test_size=0.2, random_state=156)

# 학습 데이터를 다시 학습과 검증 데이터로 분리(학습:9 / 검증:1)
X_tr, X_val, y_tr, y_val = train_test_split(X_train, y_train, test_size=0.1, random_state=156)

○모델 성능 평가 함수 선언

from sklearn.metrics import *

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))

○HyperOpt 설정 1 - 검색 공간 설정

  • 하이퍼 파라미터 검색 공간 설정(정수형 하이퍼 파라미터 → hp.quniform() 사용)
# HyperOpt 검색 공간 설정
from hyperopt import hp

xgb_search_space = {'max_depth':hp.quniform('max_depth', 5, 20, 1),
                   'min_child_weight':hp.quniform('min_child_weight', 1, 2, 1),
                   'learning_rate':hp.uniform('learning_rate', 0.01, 0.2),
                   'colsample_bytree':hp.uniform('colsample_bytree', 0.5, 1)}

○HyperOpt 설정 2 - 목적 함수 설정

  • 검색 공간에서 설정한 하이퍼 파라미터들을 입력받아서 XGBoost를 학습하고, 평가 지표를 반환하도록 구성

한 가지 아쉬운 점은 XGBoost, LightGBM에서는 cross_val_score() 적용할 경우 조기 중단(early stopping)이 지원되지 않음. 따라서 조기 중단을 하기 위해서 KFold로  학습과 검증용 데이터 세트를 만들어서 직접 교차검증을 수행해야 함

(참고:https://www.inflearn.com/questions/794506/hyperopt%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%95%98%EC%9D%B4%ED%8D%BC%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%ED%8A%9C%EB%8B%9D)

# HyperOpt 목적 함수 설정
from sklearn.model_selection import cross_val_score # 교차검증
from xgboost import XGBClassifier
from hyperopt import STATUS_OK

def objective_func(search_space):
    xgb_clf = XGBClassifier(
        n_estimators=100,
        max_depth=int(search_space['max_depth']), # 정수형 하이퍼 파라미터 형변환 필요:int형
        min_child_weight=int(search_space['min_child_weight']), # 정수형 하이퍼 파라미터 형변환 필요:int형
        learning_rate=search_space['learning_rate'],
        colsample_bytree=search_space['colsample_bytree'],
        eval_metric='logloss')
    
    # 목적 함수의 반환값은 교차검증 기반의 평균 정확도 사용
    accuracy = cross_val_score(xgb_clf, X_train, y_train, scoring='accuracy', cv=3)
    
    # accuracy는 cv=3 개수만큼의 결과를 리스트로 가짐. 이를 평균하여 반환하되 -1을 곱함
    return {'loss':-1 * np.mean(accuracy), 'status':STATUS_OK}

○HyperOpt 설정 3 - fmin()을 이용해 최적 하이퍼 파라미터 도출

# HyperOpt fmin()을 이용해 최적 하이퍼 파라미터 도출
from hyperopt import fmin, tpe, Trials

trial_val = Trials()

best = fmin(fn=objective_func,
           space=xgb_search_space,
           algo=tpe.suggest,
           max_evals=50, # 입력값 시도 횟수 지정
           trials=trial_val,
           # rstate=np.random.default_rng(seed=9)
           )

print('best:', best)

정수형 하이퍼 파라미터(max_depth, min_child_weight) 값이 실수형으로 도출됨을 유의하기

○추출된 최적 하이퍼 파라미터를 이용하여 XGBoost의 인자로 입력하여 학습 및 평가

  • 입력하기 전에 정수형 하이퍼 파라미터 형변환 필수
# 도출된 최적 하이퍼 파라미터를 이용하여 모델 선언
xgb_wrapper = XGBClassifier(n_estimators=400,
                           learning_rate=round(best['learning_rate'], 5),
                           max_depth=int(best['max_depth']), # 형변환
                           min_child_weight=int(best['min_child_weight']), # 형변환
                           colsample_bytree=round(best['colsample_bytree'], 5)
                           )
# 모델 학습: 조기 중단(early stopping) - 50
evals = [(X_tr, y_tr), (X_val, y_val)]
xgb_wrapper.fit(X_tr, y_tr, early_stopping_rounds=50, eval_metric='logloss',
               eval_set=evals)

# 예측
preds = xgb_wrapper.predict(X_test)
pred_proba = xgb_wrapper.predict_proba(X_test)[:, 1]

# 모델 평가
get_clf_eval(y_test, preds, pred_proba)
[0]	validation_0-logloss:0.56908	validation_1-logloss:0.60656
[1]	validation_0-logloss:0.47637	validation_1-logloss:0.53729
[2]	validation_0-logloss:0.40361	validation_1-logloss:0.48320
... ... ...
[130]	validation_0-logloss:0.01499	validation_1-logloss:0.24451
[131]	validation_0-logloss:0.01494	validation_1-logloss:0.24374
[132]	validation_0-logloss:0.01490	validation_1-logloss:0.24530
... ... ...
[179]	validation_0-logloss:0.01335	validation_1-logloss:0.24535
[180]	validation_0-logloss:0.01332	validation_1-logloss:0.24475
[181]	validation_0-logloss:0.01330	validation_1-logloss:0.24549

오차 행렬
[[34  3]
 [ 2 75]]
정확도:0.9561, 정밀도:0.9615, 재현율:0.9740, F1:0.9677, AUC:0.9933

 

하이퍼 파라미터 튜닝 수행 방법

대표적으로 GridSearch, RandomSearch, Bayesian Optimization, 수동 튜닝이 있다.

 

하이퍼 파라미터 튜닝의 주요 이슈

●Gradient Boosting 기반 알고리즘은 튜닝 해야 할 하이퍼 파라미터 개수가 많고 범위가 넓어서 가능한 개별 경우의 수가 너무 많음

●이러한 경우의 수가 많을 경우 데이터가 크면 하이퍼 파라미터 튜닝에 굉장히 오랜 시간이 투입되어야 함

 

GridSearch와 RandomSearch의 주요 이슈

GridSearch 방식으로 하이퍼 파라미터 튜닝을 한다면 5*4*6*6*4*4=11520회에 걸쳐서 반복적으로 학습과 평가를 수행해야만 하기에 수행 시간이 매우 오래 걸리고, 교차검증까지 같이 진행하면 cv 값 만큼 곱해져서 수행 시간이 매우 오래 걸림

 

●GridSearchCV는 수행 시간이 너무 오래 걸림. 개별 하이퍼 파라미터들은 Grid 형태로 지정하는 것은 한계가 존재(데이터 세트가 작을 때 유리)

●RandomizedSearch는 GridSearchCV를 랜덤하게 수행하면서 수행 시간은 줄여 주지만, Random한 선택으로 최적 하이퍼 파라미터 검출에 태생적 제약(데이터 세트가 클 때 유리)

●두 가지 방법 모두 iteration 중에 어느정도 최적화된 하이퍼 파라미터들을 활용하면서 최적화를 수행할 수 없음

 

Bayesian 최적화가 필요한 순간

●가능한 최소의 시도로 최적의 답을 찾아야 할 경우

●개별 시도가 너무 많은 시간/자원이 필요할 때


베이지안 최적화 개요

●베이지안 최적화는 알려지지 않은 목적 함수(블랙 박스 형태의 함수)에서, 최대 또는 최소의 함수 반환값을 만드는 최적해(최적 입력값)를 짧은 반복을 통해 찾아내는 방식

 

베이지안 확률에 기반을 두고 있는 최적화 기법

  • 베이지안 확률이 새로운 데이터를 기반으로 사후 확률을 개선해 나가듯이, 베이지안 최적화는 새로운 데이터를 입력받았을 때 최적 함수를 예측하는 사후 모델을 개선해 나가면서 최적 함수 모델을 만들어 낸다.

●베이지안 최적화의 주요 요소

1. 대체 모델(Surrogate Model)

2. 획득 함수(Acquisition Function)

 

대체 모델은 획득 함수로부터 최적 함수를 예측할 수 있는 입력값을 추천 받은 뒤 이를 기반으로 최적 함수 모델을 개선해 나가며, 획득 함수는 개선된 대체 모델을 기반으로 최적 입력값을 계산하는 방식

 

이때 입력값은 하이퍼 파라미터이다. 즉, 대체 모델은 획득 함수가 계산한 하이퍼 파라미터를 입력받으면서 점차적으로 모델 개선을 수행, 획득 함수는 개선된 대체 모델을 기반으로 더 정확한 하이퍼 파라미터를 계산

 

베이지안 최적화 프로세스

1) 최초에는 랜덤하게 하이퍼 파라미터들을 샘플링하고 성능 결과를 관측

검은색 원은 특정 하이퍼 파라미터가 입력되었을 때 관측된 성능 지표 결과값을 뜻하며 주황색 사선은 찾아야 할 목표 최적함수 이다.

 

2) 관측된 값을 기반으로 대체 모델은 최적 함수를 추정

파란색 실선은 대체 모델이 추정한 최적 함수이다. 하늘색 영역은 예측된 예측된 함수의 신뢰 구간으로, 추정된 함수의 결과값 오류 편차를 의미하며 추정 함수의 불확실성을 나타낸다.

최적 관측값은 y축 value에서 가장 높은 값을 가질 때의 하이퍼 파라미터이다.

 

3) 추정된 최적 함수를 기반으로 획득 함수에서 다음으로 관측할 하이퍼 파라미터 추천

획득 함수는 이전의 최적 관측값보다 더 큰 최대값을 가질 가능성이 높은 지점을 찾아서 다음에 관측할 하이퍼 파라미터를 대체 모델에 전달한다.

 

4) 획득 함수로부터 전달된 하이퍼 파라미터로 관측된 값을 기반으로 대체 모델은 갱신되어 다시 최적 함수 예측 추정

 

이런 방식으로 3번과 4번 과정을 특정 횟수만큼 반복하게 되면 대체 모델의 불확실성이 개선되고 점차 정확한 최적 함수 추정이 가능해진다.


HyperOpt 사용법

대체 모델은 최적 함수를 추정할 때 다양한 알고리즘을 사용할 수 있다. 일반적으로는 가우시안 프로세스(Gaussian Process)를 적용하지만, HyperOpt는 트리 파르젠 Estimator(TPE, Tree-structure Parzen Estimator)를 사용한다.

 

HyperOpt는 다음과 같은 로직을 통해 사용할 수 있다.

1. 입력 변수명과 입력값의 검색 공간(Search Space) 설정

2. 목적 함수 설정

3. fmin() 함수를 통해 베이지안 최적화 기법에 기반한 목적 함수의 반환값이 최솟값을 가지는 최적 입력값 유추

 

설치

!pip install hyperopt

검색 공간 설정

# hp 모듈은 입력값의 검색 공간을 다양하게 설정할 수 있도록 여러 함수를 제공

  • hp.quniform(label, low, high, q): label로 지정된 입력 변수명 검색 공간을 최솟값 low에서 최대값 high까지 q의 간격을 가지고 설정
  • hp.uniform(label, low, high): 최솟값 low에서 최대값 high까지 정규 분포 형태의 검색 공간 설정
  • hp.randint(label, upper): 0부터 최대값 upper까지 random한 정수값으로 검색 공간 설정
  • hp.loguniform(label, low, high): exp(uniform(low, high)값을 반환하며, 반환 값의 log 변환된 값은 정규 분포 형태를 가지는 검색 공간 설정
  • hp.choice(label, options): 검색 값이 문자열 또는 문자열과 숫자값이 섞여 있을 경우 설정. options는 리스트나 튜플 형태로 제공되며 hp.choice('tree_criterion', ['gini', 'entropy'])와 같이 설정하면 입력 변수 tree_criterion의 값을 'gini'와 'entropy'로 설정하여 입력함
from hyperopt import hp

# -10 ~ 10까지 1간격을 가지는 입력 변수 x, -15 ~ 15까지 1간격을 가지는 입력 변수 y 설정
search_space = {'x':hp.quniform('x', -10, 10, 1), 'y':hp.quniform('y', -15, 15, 1)}

목적 함수 생성

from hyperopt import STATUS_OK

# 목적 함수를 생성. 변수값과 변수 검색 공간을 가지는 딕셔너리를 인자로 받고, 특정값을 반환
def objective_func(search_space):
    x = search_space['x']
    y = search_spaec['y']
    retval = x**2 -20*y # retval = x**2-20*y로 계산된 값을 반환하게 설정한 이유는 예제를 위해서 임의로 만든것

    return retval
# hyperopt에서 목적 함수의 반환값은 권장하는 양식이 다음과 같음
# return {'loss':retval, 'status':STATUS_OK}
# return retval 만 해도 잘 반환됨

fmin() 함수를 통해 베이지안 최적화 기법에 기반한 목적 함수의 반환값이 최솟값을 가지는 최적 입력값 유추

# fmin() 함수의 주요 인자

  • fn: 위에서 생성한 objective_func와 같은 목적 함수
  • space: 위에서 생성한 search_space와 같은 검색 공간
  • algo: 베이지안 최적화 적용 알고리즘. 기본적으로 tpe.suggest이며 이는 HyperOpt의 기본 최적화 알고리즘인 TPE(Tree of Parzen Estimator)를 의미
  • max_evals: 최적 입력값을 찾기 위한 입력값 시도 횟수
  • trials: 최적 입력값을 찾기 위해 시도한 입력값 및 해당 입력값의 목적 함수 반환값 결과를 저장하는데 사용. Trials 클래스를 객체로 생성한 변수명을 입력함
  • rstate: fmin()을 수행할 때마다 동일한 결과값을 가질 수 있도록 설정하는 랜덤 시드 값

rstate는 예제 결과와 동일하게 만들기 위해 적용했으며, 경험적으로 적용 안하는 것이 더 좋은 결과를 반환한 경우가 많았음→일반적으로 rstate를 잘 적용하지 않음

HyperOpt는 rstate에 넣어주는 인자값으로 일반적인 정수형 값을 넣지 않는다. 또한 버전별로 rstate 인자값이 조금씩 다르다. 현 버전인 0.2.7에서는 넘파이의 random Generator를 생성하는 random.default_rng() 함수 인자로 seed값을 입력하는 방식

from hyperopt import fmin, tpe, Trials
import numpy as np

# 입력값 및 해당 입력값의 목적 함수 반환값 결과를 저장할 Trials 객체값 생성
trial_val = Trials()

# 목적 함수의 반환값이 최솟값을 가지는 최적 입력값을 5번의 입력값 시도(max_evals=5)로 찾아냄
best_01 = fmin(fn=objective_func, space=search_space, algo=tpe.suggest, max_evals=5,
              trials=trial_val, rstate=np.random.default_rng(seed=0))
print('best:', best_01)

입력 변수 x의 공간 -10 ~ 10, y의 공간 -15 ~ 15에서 목적 함수의 반환값을 x**2 -20*y로 설정했으므로 x는 0에 가까울수록 y는 15에 가까울수록 반환값이 최소로 근사될 수 있다. 확실하게 만족할 수준의 최적 x와 y값을 찾은 것은 아니지만, 5번의 수행으로 어느 정도 최적값에 다가설 수 있었다는 점은 주지할만하다.

trial_val = Trials()

# max_evals를 20회로 늘려서 재테스트
best_02 = fmin(fn=objective_func, space=search_space, algo=tpe.suggest, max_evals=20,
              trials=trial_val, rstate=np.random.default_rng(seed=0))
print('best:', best_02)

max_evals=20으로 재테스트 시 x는 2, y는 15로 만족할 수준의 최적 x와 y값을 찾음

 

만일 그리드 서치와 같이 순차적으로 x, y 변수값을 입력해서 최소 함수 반환값을 찾는다면, 입력값 x는 -10 ~ 10까지 21개의 경우의 수, 입력값 y는 -15 ~ 15까지 31개의 경우의 수로 21*31=651회의 반복이 필요한데, 베이지안 최적화를 이용해서 는 20회의 반복만으로 일정 수준의 최적값을 근사해 낼 수 있다.

→베이지안 최적화 방식은 상대적으로 최적값을 찾는 시간을 많이 줄여 줌


Trials 객체 더 알아보기

 

Trials 객체의 중요 속성

  • results: 함수의 반복 수행 시마다 반환되는 반환값
  • vals: 함수의 반복 수행 시마다 입력되는 입력 변수값
# result: 리스트 내부의 개별 원소는 {'loss':함수 반환값, 'status':반환 상태값}와 같은 딕셔너리
print(trial_val.results)

# vals: {'입력변수명':개별 수행 시마다 입력된 값의 리스트}와 같은 형태
print(trial_val.vals)

Trials 객체의 result와 vals 속성은 HyperOpt의 fmin() 함수의 수행 시마다 최적화되는 경과를 볼 수 있는 함수 반환값과 입력 변수값들의 정보를 제공

가독성을 높이기 위해 DataFrame으로 만들어서 확인해보자!

import pandas as pd

# result에서 loss 키값에 해당하는 value를 추출하여 list로 생성
losses = [loss_dict['loss'] for loss_dict in trial_val.results]

# DataFrame으로 생성
result_df = pd.DataFrame({'x':trial_val.vals['x'], 'y':trial_val.vals['y'], 'losses':losses})
result_df

 

+ Recent posts