9 분 소요

분류

베이지안 최적화 기반의 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: 최초에는 랜덤하게 하이퍼 파라미터들을 샘플링하여 성능 결과를 관측

IMG_2569

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

IMG_2568

Step 3: 획득 함수에서 다음으로 관측할 하이퍼 파라미터 추출

IMG_2567

Step 4: 해당 하이퍼 파라미터로 관측된 값을 기반으로 대체 모델은 다시 최적 함수 예측 추정

IMG_2566

베이지안 최적화 구현 요소

  1. 입력값 범위
    search_space = {'x': (-10, 10), 'y': (-15, 15)}
    
  2. 함수
    def black_box_function(x, y):  
      return -x**2 - 20*y
    
  3. 함수 반환 최솟값 유추
    fmin(fn=black_box_function, space=search_space,
    algo=tpe.suggest, max_evals=20, trials=trial_val)
    

베이지안 최적화를 구현한 주요 패키지

  • HyperOpt
  • Bayesian optimization
  • Optuna

HyperOpt 사용하기

HyperOpt를 통한 최적화 예시

  1. Search Sapce (입력값 범위)
    search_space = {'x': hp.quniform('x', 5, 15, 1),
                 'y': hp.uniform('y', 0.01, 01)}
    
  2. 목적 함수
    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}
    
  3. 목적 함수 반환 최솟값 유추
    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