[Python 머신러닝] 04-8 베이지안 최적화 기반의 HyperOpt를 이용한 하이퍼 파라미터 튜닝
분류
베이지안 최적화 기반의 HyperOpt를 이용한 하이퍼 파라미터 튜닝
하이퍼 파라미터 튜닝 수행 방법
- Grid Search
- Random Search
- Bayesian Optimization
- 수동 튜닝
하이퍼 파라미터 튜닝의 주요 이슈
- Gridient Boosting 기반 알고리즘은 튜닝 해야 할 하이퍼 파라미터 개수가 많고 범위가 넓어서 가능한 개별 경우의 수가 너무 많음
- 이러한 경우의 수가 많을 경우 데이터가 크면 하이퍼 파라미터 튜닝에 굉장히 오랜 시간이 투입되어야 함
Grid Search와 Random Search의 주요 이슈
GridSearchCV(classifier, params, cv=3) / RandomizedSearch(classifier, parmas, cv=3, n_iter=10)
params = {
'max_depth' = [10, 20, 30, 40, 50],
'num_leaves' = [35, 45, 55, 65],
'colsample_bytree' = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
'subsample' = [0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
'max_bin' = [100, 200, 300, 400],
'min_child_weight' = [10, 20, 30, 40]
}
- GridSearchCV는 수행 시간이 너무 오래 걸림
개별 하이퍼 파라미터들을 Grid 형태로 지정하는 것은 한계가 존재 (데이터 세트가 작을 때 유리) - RandomizedSearch는 수행 시간은 줄여 주지만, Random한 선택으로 최적 하이퍼 파라미터 검출에 태생적 제약 (데이터 세트가 클 때 유리)
- 두 가지 방법 모두 iteration 중에 어느 정도 최적화된 하이퍼 파라미터들을 활용하면서 최적화를 수행할 수 없음
Bayesian 최적화가 필요한 순간
- 가능한 최소의 시도로 최적의 답을 찾아야 할 경우
- 개별 시도가 너무 많은 시간/자원이 필요할 때
베이지안 최적화 개요
- 베이지안 최적화는 미지의 함수가 반환하는 값의 최소 또는 최대값을 만드는 최적해를 짧은 반복을 통해 찾아내는 최적화 방식
- 베이지안 최적화는 새로운 데이터를 입력 받았을 때 최적 함수를 예측하는 사후 모델을 개선해 나가면서 최적 함수를 도출
- 대체 모델(Surrogate Model)과 획득 함수로 구성되며, 대체 모델은 획득 함수로 부터 최적 입력 값을 추천 받은 뒤 이를 기반으로 최적 함수 모델을 개선
- 획득 함수는 개선된 대체 모델을 기반으로 다시 최적 입력 값을 계산
베이지안 최적화 수행 단계
Step 1: 최초에는 랜덤하게 하이퍼 파라미터들을 샘플링하여 성능 결과를 관측
Step 2: 관측된 값을 기반으로 대체 모델은 최적 함수를 예측 추정
Step 3: 획득 함수에서 다음으로 관측할 하이퍼 파라미터 추출
Step 4: 해당 하이퍼 파라미터로 관측된 값을 기반으로 대체 모델은 다시 최적 함수 예측 추정
베이지안 최적화 구현 요소
- 입력값 범위
search_space = {'x': (-10, 10), 'y': (-15, 15)} - 함수
def black_box_function(x, y): return -x**2 - 20*y - 함수 반환 최솟값 유추
fmin(fn=black_box_function, space=search_space, algo=tpe.suggest, max_evals=20, trials=trial_val)
베이지안 최적화를 구현한 주요 패키지
- HyperOpt
- Bayesian optimization
- Optuna
HyperOpt 사용하기
HyperOpt를 통한 최적화 예시
- Search Sapce (입력값 범위)
search_space = {'x': hp.quniform('x', 5, 15, 1), 'y': hp.uniform('y', 0.01, 01)} - 목적 함수
def objective_func(search_space): x = search_space['x'] y = search_space['y'] print('x:', x, 'y:', y) return{'loss': x**2 + y*20, 'status': STATUS_OK} - 목적 함수 반환 최솟값 유추
best = fmin(fn=objective_func, space=search_space, algo=algo, max_evals=5, trials=trials)
[output]
[{'loss': 81.20833131199375},
{'loss': 169.20757538485393},
{'loss': 121.10536542037384},
{'loss': 64.08021188657003},
{'loss': 81.42067134007004}]
HyperOpt의 주요 구성 요소
| 구성 요소 | 설명 |
|---|---|
| search_space (입력값 범위) |
* 여러 개의 입력 변수들과 이들 값의 범위를 지정 * 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 변환 된 값은 정규 분포 형태를 가지는 검색 공간 설정 |
| 목적 함수 | * serach space를 입력 받아 로직에 따라 loss값을 계산하고 이를 반환하는 함수 반드시 dictionary 형태의 값을 반환하고 여기에 ‘loss’: loss값이 기재되어야 함 |
| 목적 함수의 최솟값을 찾는 함수 | * 목적 함수를 실행하여 최소 반환값(loss)을 최적으로 찾아 내는 함수 Bayesian 최적화 기법으로 입력 변수들의 search space 상에서 정해진 횟수만큼 입력하여 목적 함수의 반환값(loss)을 최적으로 찾아냄 hyperopt는 일르 위해 fmin( ) 함수를 제공 fmin( ) 함수의 인자로 목적 함수, search space, 베이지안 최적화 기법 유형, 최적화 시도 횟수, 최적화 로그 기록 객체를 인자로 넣어줌 best = fmin(objective, sapce=hp.uniform(‘x’, -10, 10), algo=tpe.suggest, max_evals=100, trials=trials) |
<실습>실습>
import hyperopt
print(hyperopt.__version__)
0.2.7
#!pip install hyperopt==0.2.7
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) }
search_space
{'x': <hyperopt.pyll.base.Apply at 0x1db7dd926d0>,
'y': <hyperopt.pyll.base.Apply at 0x1db006dd130>}
-> search_space는 객체로 되어 있음
from hyperopt import STATUS_OK
# 목적 함수를 생성. 입력 변수값과 입력 변수 검색 범위를 가지는 딕셔너리를 인자로 받고, 특정 값을 반환
def objective_func(search_space):
x = search_space['x']
y = search_space['y']
retval = x**2 - 20*y
return retval # return {'loss': retval, 'status':STATUS_OK}
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)
100%|████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 1109.37trial/s, best loss: -224.0]
best: {'x': -4.0, 'y': 12.0}
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)
100%|██████████████████████████████████████████████████████████| 20/20 [00:00<00:00, 1176.21trial/s, best loss: -296.0]
best: {'x': 2.0, 'y': 15.0}
trial_val
<hyperopt.base.Trials at 0x1db006dd370>
- HyperOpt 수행 시 적용된 입력 값들과 목적 함수 반환값 보기
# fmin( )에 인자로 들어가는 Trials 객체의 result 속성에 파이썬 리스트로 목적 함수 반환값들이 저장됨
# 리스트 내부의 개별 원소는 {'loss':함수 반환값, 'status':반환 상태값} 와 같은 딕셔너리임.
print(trial_val.results)
[{'loss': -64.0, 'status': 'ok'}, {'loss': -184.0, 'status': 'ok'}, {'loss': 56.0, 'status': 'ok'}, {'loss': -224.0, 'status': 'ok'}, {'loss': 61.0, 'status': 'ok'}, {'loss': -296.0, 'status': 'ok'}, {'loss': -40.0, 'status': 'ok'}, {'loss': 281.0, 'status': 'ok'}, {'loss': 64.0, 'status': 'ok'}, {'loss': 100.0, 'status': 'ok'}, {'loss': 60.0, 'status': 'ok'}, {'loss': -39.0, 'status': 'ok'}, {'loss': 1.0, 'status': 'ok'}, {'loss': -164.0, 'status': 'ok'}, {'loss': 21.0, 'status': 'ok'}, {'loss': -56.0, 'status': 'ok'}, {'loss': 284.0, 'status': 'ok'}, {'loss': 176.0, 'status': 'ok'}, {'loss': -171.0, 'status': 'ok'}, {'loss': 0.0, 'status': 'ok'}]
# Trials 객체의 vals 속성에 {'입력변수명':개별 수행 시마다 입력된 값 리스트} 형태로 저장됨.
print(trial_val.vals)
{'x': [-6.0, -4.0, 4.0, -4.0, 9.0, 2.0, 10.0, -9.0, -8.0, -0.0, -0.0, 1.0, 9.0, 6.0, 9.0, 2.0, -2.0, -4.0, 7.0, -0.0], 'y': [5.0, 10.0, -2.0, 12.0, 1.0, 15.0, 7.0, -10.0, 0.0, -5.0, -3.0, 2.0, 4.0, 10.0, 3.0, 3.0, -14.0, -8.0, 11.0, -0.0]}
import pandas as pd
# results에서 loss 키값에 해당하는 밸류들을 추출하여 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
| x | y | losses | |
|---|---|---|---|
| 0 | -6.0 | 5.0 | -64.0 |
| 1 | -4.0 | 10.0 | -184.0 |
| 2 | 4.0 | -2.0 | 56.0 |
| 3 | -4.0 | 12.0 | -224.0 |
| 4 | 9.0 | 1.0 | 61.0 |
| 5 | 2.0 | 15.0 | -296.0 |
| 6 | 10.0 | 7.0 | -40.0 |
| 7 | -9.0 | -10.0 | 281.0 |
| 8 | -8.0 | 0.0 | 64.0 |
| 9 | -0.0 | -5.0 | 100.0 |
| 10 | -0.0 | -3.0 | 60.0 |
| 11 | 1.0 | 2.0 | -39.0 |
| 12 | 9.0 | 4.0 | 1.0 |
| 13 | 6.0 | 10.0 | -164.0 |
| 14 | 9.0 | 3.0 | 21.0 |
| 15 | 2.0 | 3.0 | -56.0 |
| 16 | -2.0 | -14.0 | 284.0 |
| 17 | -4.0 | -8.0 | 176.0 |
| 18 | 7.0 | 11.0 | -171.0 |
| 19 | -0.0 | -0.0 | 0.0 |
HyperOpt를 이용한 XGBoost 하이퍼 파라미터 최적화
<실습>실습>
import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
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
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 )
# 학습 데이터를 다시 학습과 검증 데이터로 분리
X_tr, X_val, y_tr, y_val= train_test_split(X_train, y_train,
test_size=0.1, random_state=156 )
from hyperopt import hp
# max_depth는 5에서 20까지 1간격으로, min_child_weight는 1에서 2까지 1간격으로
# colsample_bytree는 0.5에서 1사이, learning_rate는 0.01에서 0.2사이 정규 분포된 값으로 검색.
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)
}
from sklearn.model_selection import cross_val_score
from xgboost import XGBClassifier
from hyperopt import STATUS_OK
# fmin()에서 입력된 search_space값으로 입력된 모든 값은 실수형임.
# XGBClassifier의 정수형 하이퍼 파라미터는 정수형 변환을 해줘야 함.
# 정확도는 높은 수록 더 좋은 수치임. -1* 정확도를 곱해서 큰 정확도 값일 수록 최소가 되도록 변환
def objective_func(search_space):
# 수행 시간 절약을 위해 n_estimators는 100으로 축소
xgb_clf = XGBClassifier(n_estimators=100, max_depth=int(search_space['max_depth']),
min_child_weight=int(search_space['min_child_weight']),
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}
-> * max-depth는 정수형 값만 입력 받는데 search_space[ ]는 실수형(5.0, 6.0, …) 값으로 반환되므로 정수형으로 형변환 필요
* accuracy는 값이 클수록 좋은 값인데 xgb_clf는 최솟값을 최적값으로 하므로 -1을 곱해서 원래 가장 큰 값이 가장 작은 값이 될 수 있도록 함
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)
100%|███████████████████████████████████████████████| 50/50 [00:29<00:00, 1.71trial/s, best loss: -0.9670616939700244]
best: {'colsample_bytree': 0.5424149213362504, 'learning_rate': 0.12601372924444681, 'max_depth': 17.0, 'min_child_weight': 2.0}
print('colsample_bytree:{0}, learning_rate:{1}, max_depth:{2}, min_child_weight:{3}'.format(
round(best['colsample_bytree'], 5), round(best['learning_rate'], 5),
int(best['max_depth']), int(best['min_child_weight'])))
colsample_bytree:0.54241, learning_rate:0.12601, max_depth:17, min_child_weight:2
from sklearn.metrics import confusion_matrix, accuracy_score
from sklearn.metrics import precision_score, recall_score
from sklearn.metrics import f1_score, roc_auc_score
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 = roc_auc_score(y_test, pred_proba)
print('오차 행렬')
print(confusion)
# ROC-AUC print 추가
print('정확도: {0:.4f}, 정밀도: {1:.4f}, 재현율: {2:.4f},\
F1: {3:.4f}, AUC:{4:.4f}'.format(accuracy, precision, recall, f1, roc_auc))
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)
)
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, verbose=True)
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.58942 validation_1-logloss:0.62048
.
. (생략)
.
[185] validation_0-logloss:0.01433 validation_1-logloss:0.22533
[186] validation_0-logloss:0.01431 validation_1-logloss:0.22426
.
. (생략)
.
[235] validation_0-logloss:0.01322 validation_1-logloss:0.22743
오차 행렬
[[35 2]
[ 2 75]]
정확도: 0.9649, 정밀도: 0.9740, 재현율: 0.9740, F1: 0.9740, AUC:0.9944
losses = [loss_dict['loss'] for loss_dict in trial_val.results]
result_df = pd.DataFrame({'max_depth': trial_val.vals['max_depth'],
'min_child_weight': trial_val.vals['min_child_weight'],
'colsample_bytree': trial_val.vals['colsample_bytree'],
'learning_rate': trial_val.vals['learning_rate'],
'losses': losses
}
)
result_df
| max_depth | min_child_weight | colsample_bytree | learning_rate | losses | |
|---|---|---|---|---|---|
| 0 | 19.0 | 2.0 | 0.585235 | 0.033688 | -0.947296 |
| 1 | 5.0 | 2.0 | 0.727186 | 0.105956 | -0.960483 |
| 2 | 6.0 | 2.0 | 0.959945 | 0.154804 | -0.958290 |
| 3 | 6.0 | 2.0 | 0.950012 | 0.120686 | -0.960468 |
| 4 | 16.0 | 2.0 | 0.674336 | 0.142392 | -0.962661 |
| 5 | 8.0 | 2.0 | 0.863774 | 0.106579 | -0.958275 |
| 6 | 14.0 | 2.0 | 0.957521 | 0.079111 | -0.956097 |
| 7 | 19.0 | 2.0 | 0.695018 | 0.095213 | -0.960468 |
| 8 | 9.0 | 2.0 | 0.684442 | 0.147520 | -0.962661 |
| 9 | 8.0 | 1.0 | 0.592116 | 0.081179 | -0.956097 |
| 10 | 6.0 | 2.0 | 0.614798 | 0.076255 | -0.956082 |
| 11 | 7.0 | 2.0 | 0.776738 | 0.089624 | -0.960468 |
| 12 | 8.0 | 2.0 | 0.514772 | 0.092214 | -0.958275 |
| 13 | 19.0 | 1.0 | 0.949783 | 0.083983 | -0.949474 |
| 14 | 10.0 | 1.0 | 0.926121 | 0.112477 | -0.949489 |
| 15 | 6.0 | 2.0 | 0.570990 | 0.064663 | -0.958290 |
| 16 | 7.0 | 2.0 | 0.884549 | 0.042766 | -0.949489 |
| 17 | 18.0 | 2.0 | 0.548302 | 0.184028 | -0.962647 |
| 18 | 6.0 | 2.0 | 0.910278 | 0.133006 | -0.960468 |
| 19 | 9.0 | 2.0 | 0.532501 | 0.091771 | -0.964869 |
| 20 | 15.0 | 1.0 | 0.644890 | 0.189043 | -0.958275 |
| 21 | 11.0 | 1.0 | 0.780915 | 0.154057 | -0.960468 |
| 22 | 11.0 | 2.0 | 0.510122 | 0.169793 | -0.960483 |
| 23 | 16.0 | 1.0 | 0.822165 | 0.054728 | -0.947296 |
| 24 | 13.0 | 2.0 | 0.647444 | 0.011072 | -0.936316 |
| 25 | 17.0 | 2.0 | 0.542415 | 0.126014 | -0.967062 |
| 26 | 17.0 | 1.0 | 0.538160 | 0.128161 | -0.962676 |
| 27 | 12.0 | 2.0 | 0.506463 | 0.010147 | -0.940717 |
| 28 | 13.0 | 2.0 | 0.616162 | 0.170540 | -0.958275 |
| 29 | 20.0 | 2.0 | 0.564500 | 0.025787 | -0.942924 |
| 30 | 10.0 | 2.0 | 0.733826 | 0.058339 | -0.951696 |
| 31 | 14.0 | 2.0 | 0.501102 | 0.119548 | -0.967062 |
| 32 | 15.0 | 2.0 | 0.597853 | 0.170319 | -0.960454 |
| 33 | 17.0 | 1.0 | 0.501951 | 0.113862 | -0.962676 |
| 34 | 14.0 | 2.0 | 0.709170 | 0.135741 | -0.960454 |
| 35 | 18.0 | 2.0 | 0.999433 | 0.199366 | -0.960454 |
| 36 | 15.0 | 2.0 | 0.651538 | 0.122986 | -0.960454 |
| 37 | 20.0 | 2.0 | 0.839988 | 0.101882 | -0.958275 |
| 38 | 16.0 | 2.0 | 0.765179 | 0.149996 | -0.956053 |
| 39 | 14.0 | 1.0 | 0.613403 | 0.139308 | -0.958290 |
| 40 | 17.0 | 2.0 | 0.666513 | 0.102078 | -0.962661 |
| 41 | 18.0 | 2.0 | 0.559546 | 0.069568 | -0.960483 |
| 42 | 12.0 | 2.0 | 0.527415 | 0.161834 | -0.967062 |
| 43 | 12.0 | 2.0 | 0.588290 | 0.160257 | -0.962661 |
| 44 | 19.0 | 1.0 | 0.804978 | 0.116651 | -0.951696 |
| 45 | 13.0 | 2.0 | 0.696878 | 0.145955 | -0.964854 |
| 46 | 11.0 | 2.0 | 0.524901 | 0.181720 | -0.964854 |
| 47 | 10.0 | 2.0 | 0.725896 | 0.198962 | -0.964840 |
| 48 | 12.0 | 1.0 | 0.630900 | 0.107408 | -0.960497 |
| 49 | 14.0 | 2.0 | 0.675242 | 0.125260 | -0.960468 |