- 테스트 데이터(검증 데이터)에 특정 피처의 값들을 반복적으로 무작위로 섞은 뒤 모델 성능이 얼마나 저하되는지를 기준으로 해당 피처의 중요도를 산정
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로 모델 성능이 가장 저하해서 가장 중요한 피처이다.
- 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 사이에 특정 값이 최적의 피처 개수인듯
# 선택된 피처를 통한 최종 데이터프레임 생성
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()])
사이킷런의 Recursive Feature Elimination이나 SelectFromModel의 경우 트리기반의 분류에서는 Feature importance, 회귀에서는 회귀 계수를 기반으로 반복적으로 모델 평가하면서 피처들을 선택하는 방식인데, 이런 방식의 Feature Selection은 오히려 모델의 성능을 떨어뜨릴 가능성이 높다. 그러니까 정확하지 않은 feature selection 방법이다.
개별 알고리즘의 예측 결과 데이터 세트를 최종적인 메타 데이터 세트로 만들어 별도의 ML 알고리즘으로 최종 학습을 수행하고 테스트 데이터를 기반으로 다시 최종 예측을 수행하는 방식
→기반 모델들이 예측한 값들을 Stacking 형태로 만들어서 메타 모델이 이를 학습하고 예측하는 모델
●스태킹 모델은 다음과 같은 두 종류의 모델이 필요함
개별적인 기반 모델
이 개별 기반 모델의 예측 데이터를 학습 데이터로 만들어서 학습하는 최종 메타 모델
즉, 스태킹 모델은 여러 개별 모델의 예측 데이터를 각각 스태킹 형태로 결합해 최종 메타 모델의 학습용 피처 데이터 세트와 테스트용 피처 데이터 세트를 만드는 것
기본 스태킹
# 필요한 모듈 불러오기
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
# 인자로 입력받은 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
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 피처는 신용 카드 사용 금액으로 정상/사기 트랜잭션을 결정하는 매우 중요한 피처일 가능성이 높다.
모든 피처들의 이상치를 검출하는 것은 시간이 많이 소모되며, 결정값과 상관성이 높지 않은 피처들의 경우는 이상치를 제거하더라도 크게 성능 향상에 기여하지 않기 때문에 매우 많은 피처가 있을 경우 이들 중 결정값과 가장 상관성이 높은 피처들을 위주로 이상치를 검출하는 것이 좋다.
상관관계 히트맵에서 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
재샘플링, 즉 오버 샘플링 및 언더 샘플링을 적용시 올바른 평가를 위해 반드시 학습 데이터 세트에만 적용해야함
●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())
threshold가 0.99 이하에서는 재현율이 매우 좋고, 정밀도가 극단적으로 낮다가 0.99 이상에서는 반대로 재현율이 대폭 떨어지고 정밀도가 높아짐. threshold를 조정하더라도 threshold의 민감도가 너무 심해 올바른 재현율/정밀도 성능을 얻을 수 없으므로 로지스틱 회귀 모델의 경우 SMOTE 적용 후 올바른 예측 모델이 생성되지 못했음.
LightGBM의 모델 성능 결과를 보면, 재현율은 증가했지만 정밀도가 감소했음을 알 수 있음. 이처럼 SMOTE를 적용하면 재현율은 높아지나, 정밀도는 낮아지는 것이 일반적임. 때문에 정밀도 지표보다 재현율 지표를 높이는 것이 중요한 업무에서SMOTE를 사용하면 좋음!
'데이터 전처리-머신러닝 모델의 성능 향상을 위한 전처리-변수 분포 문제-변수 치우침 제거' 참고
'파이썬 머신러닝 완벽가이드/[2장] 사이킷런으로 시작하는 머신러닝/스케일링, 로그변환' 참고
log변환은 왜곡된 분포를 가진 피처를 비교적 정규분포에 가깝게 변환
IQR를 이용한 이상치 제거
'데이터 전처리-머신러닝 모델의 성능 향상을 위한 전처리-변수 분포 문제-이상치 제거' 참고
언더 샘플링과 오버 샘플링
'데이터 전처리-머신러닝 모델의 성능 향상을 위한 전처리-클래스 불균형 문제, 클래스 불균형 문제 해결방법' 참고
●레이블이 불균형한 분포를 가진 데이터 세트를 학습 시, 이상 레이블을 가지는 데이터 건수가 정상 레이블을 가진 데이터 건수에 비해 너무 적어 제대로 된 유형의 학습이 어려움. 즉, 이상 레이블을 가지는 데이터 건수는 매우 적기 때문에 제대로 다양한 유형을 학습하지 못하는 반면에 정상 레이블을 가지는 데이터 건수는 매우 많기 때문에 일방적으로 정상 레이블로 치우친 학습을 수행해 제대로 된 이상 데이터 검출이 어려움
●지도학습 분류에서 불균형한 레이블 값 분포로 인한 문제를 해결하는 대표적인 방법은 오버 샘플링과 언더 샘플링이 있음
# 학습 데이터, 테스트 데이터 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개 피처
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')
# 전체 데이터 중 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)
하이퍼 파라미터 검색 공간 설정(정수형 하이퍼 파라미터 → 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로 학습과 검증용 데이터 세트를 만들어서 직접 교차검증을 수행해야 함
대표적으로 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번의 수행으로 어느 정도 최적값에 다가설 수 있었다는 점은 주지할만하다.
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