Bayesian Optimization 코드 (Optuna, HyperOpt)

2025. 4. 23. 16:08·ML & DL/데이터마이닝

데이터마이닝에 관한 스터디를 진행하고자 정리한 내용입니다.

개인 공부 과정에서 틀린 부분이 있을 수 있습니다. (잘못된 부분은 알려주시면 수정하겠습니다!)


1. Optuna 사용 가이드

✅주요 개념

  • Study: 전체 최적화 과정의 단위. (Trial을 여러 번 실행하며 최적의 하이퍼파라미터를 찾아나감)
  • Trial: 특정 하이퍼파라미터 조합으로 모델을 평가하는 1번의 시도
  • Objective Function: 최적화하려는 대상 함수. ( 각 Trial의 성능을 평가하고 반환함)

0) 라이브러리 및 데이터 준비

  • optuna 없으면 `pip install optuna`로 설치하고 시작
import numpy as np
import pandas as pd
import optuna # Optuna 라이브러리
import xgboost as xgb
from sklearn.datasets import load_iris # 예시 데이터 라이브러리
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
from sklearn.model_selection import StratifiedKFold, train_test_split, cross_val_score
import matplotlib.pyplot as plt
import seaborn as sns

# 데이터 로드
iris = load_iris() # 설명을 위해 예시 데이터셋(iris) 로드
### df = pd.read_csv('~~your_data~~.csv') # 실제 데이터로 바꿔서 작성!

# 테스트세트 분할
X_train, X_test, y_train, y_test = train_test_split(iris.data, iris.target,
							test_size=0.2, stratify=y, random_state=42)

1) Objective 함수 정의

  • Optuna의 trial 객체를 인자로 받아 하이퍼파라미터 범위를 설정하는 함수
  • `trial.suggest_xxx()` 함수로 각 하이퍼파라미터의 탐색 범위를 지정
    • `suggest_int` : 정수 파라미터 (예: max_depth)
    • `suggest_float` : 실수 파라미터 (예: learning_rate) → `log=True` 설정 시 로그스케일 탐색
    • `suggest_categorical` : 카테고리컬 파라미터 (예: booster 유형) 또는 원하는 값 후보를 명시할 때
  • 모델(ex. XGBoost)을 학습하고, 성능을 평가(또는 교차검증)하여 반환함
    • cross_val_score 함수의 `scoring` 인자로 원하는 평가지표 설정 가능
    • precision_macro, recall_macro, f1_macro, roc_auc_ovr 등... 사이킷런 공식 문서에서 확인 가능
def objective(trial, X, y):
    # 최적화할 하이퍼파라미터 지정
    param = {
        'objective': 'binary:logistic', # 다중분류는 'multi:softmax'
        'booster': trial.suggest_categorical('booster', ['gbtree', 'gblinear', 'dart']),
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3),
        'n_estimators': trial.suggest_int('n_estimators', 50, 300),
        # 다른 파라미터들...
    }
    
    # 모델 생성 및 학습
    model = xgb.XGBClassifier(**param)
    
    # StratifiedKFold 적용 (선택사항)
    cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    
    # 교차검증(5-fold) 수행
    score = cross_val_score(model, X, y, cv=cv) # 기본값은 accuracy
    # score = cross_val_score(model, X, y, cv=cv, scoring='f1_macro') # scoring='f1_macro' 사용
    return score.mean()  # 평가 지표 반환 (각 폴드의 평균)

2) Study 객체 생성 & 최적화 실행

  • optuna.create_study
    • `direction` : 목적 함수 최적화 방향을 설정 (최대화 or 최소화)
    • `sampler` : TPESampler (베이지안 최적화)가 기본으로 설정되어있음
    • `pruner` : MedianPruner (중앙값 기반 가지치기)가 기본으로 설정되어있음
  • study.optimize
    • train set을 입력하여 하이퍼파라미터 튜닝 수행 (test set은 최종 평가 시 사용)
    • `n_trials` : 서로 다른 하이퍼파라미터 조합을 몇 번 시도할지 설정
  • 보다 자세한 내용은 아래의 '➕플러스알파' 참고
    • ⚠️objective 함수에 X,y를 같이 입력해주기 위해 lambda 필수 사용 (optimize에는 함수가 실행된 결과값이 아닌 함수 자체를 입력해야하기 때문!)
# Study 객체 생성
study = optuna.create_study(
    direction='maximize',                # 'maximize' 또는 'minimize'
    # sampler=optuna.samplers.TPESampler(),  # 샘플링 알고리즘 (기본값)
    # pruner=optuna.pruners.MedianPruner(),  # 가지치기 알고리즘 (기본값)
    # study_name='xgboost_optimization',    # 스터디 이름
    # storage='sqlite:///optuna_study.db'   # 결과 저장 위치
)
# study = optuna.create_study(direction='maximize') # 간단하게 이것만 써도 됨

n_trials = 100  #⭐최적화 시도 횟수 입력
pbar = tqdm(total=n_trials, desc="Optuna HP-Tunig 진행 상황") # tqdm을 활용한 진행률 표시

# 진행 상황을 업데이트하는 콜백 함수
def tqdm_callback(study, trial):
    pbar.update(1)

# 최적화 실행
study.optimize(lambda trial: objective(trial, X_train, y_train), n_trials=n_trials, callbacks=[tqdm_callback])

pbar.close()  # 진행 바 종료

3) 최적의 하이퍼파라미터 확인 & 시각화

  • 최적의 하이퍼파라미터 조합은 `study.best_params`에 저장되어있음
  • optuna.visualization 라이브러리로 결과를 시각화해볼 수 있음 (단, plotly 라이브러리 설치 필요!)
    • `plot_optimization_history` : 최적화 과정에서 목적 함수 값의 변화를 보여줌
    • `plot_param_importances` : 각 하이퍼파라미터의 중요도를 보여줌
    • `plot_parallel_coordinate` : 하이퍼파라미터와 목적 함수 값의 관계를 다차원 그래프로 보여줍니다.
    • `plot_slice` : 특정 하이퍼파라미터와 목적 함수 값의 관계를 보여줍니다.
    • `plot_contour` : 두 개의 하이퍼파라미터와 목적 함수 값의 관계를 등고선 그래프로 보여줍니다.
# 최적의 결과 출력
best_params = study.best_params # 최적의 하이퍼파라미터 조합
best_score = study.best_value  # 최적의 성능 값
print("최적의 하이퍼파라미터:", best_params)
print(f"최적의 성능 점수: {best_score:.4f}")

# 최적화 과정 시각화
fig = optuna.visualization.plot_optimization_history(study) # 목적함수 변화과정 시각화
fig.show()

fig = optuna.visualization.plot_param_importances(study) # 파라미터 중요도 확인
fig.show()

4) 최종 모델 평가 및 결과 분석

  • 앞서 하이퍼파라미터 튜닝 과정에서는 train set을 train/valid로 분리하며 교차검증(cv)을 수행했음
    → 이제 최종 성능을 평가하기 위해 따로 빼두었던 test set을 사용!
# 최적의 파라미터로 모델 생성 & 학습
final_model = xgb.XGBClassifier(**best_params) ## 딕셔너리를 인자로 전달
final_model.fit(X_train, y_train)

# 테스트 데이터로 최종 성능 평가
y_pred = final_model.predict(X_test)

print("\nClassification Report:") ## Classification Report 출력
print(classification_report(y_test, y_pred))

print("\nConfusion Matrix:") ## 혼동 행렬 시각화
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(6, 4))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('Predicted Label')
plt.ylabel('True Label')
plt.title('Confusion Matrix')
plt.show()

 

 

2. HyperOpt 사용 가이드

✅주요 개념

  • 작업 순서: 입력변수명과 입력값의 탐색 공간을 설정 → 목적함수를 설정 → 목적함수의 반환 최솟값을 갖는 최적의 파라미터를 찾아나감
  • (목적함수 최댓값을 찾는 optuna와 달리) `fmin()` 함수로 loss function을 최소화하는 방식을 사용함

0) 라이브러리 및 데이터 준비

  • hyperopt 없으면 `pip install hyperopt`로 설치하고 시작
import pandas as pd
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split

# 위스콘신 유방암 데이터셋: 31개의 변수에 대한 관측값 569개가 들어있음
# Task: 종양의 크기, 모양 등을 활용해 악성 종양/양성 종양을 분류
# 양성(benign, 1)이 357개, 악성(malignant, 0)이 212개

dataset = load_breast_cancer()
cancer_df = pd.DataFrame(data=dataset.data, columns=dataset.feature_names) # DataFrame 형태로 변환
cancer_df['target']= dataset.target

# 독립변수를 X_df, 종속변수를 Y_df로 설정
X_df = cancer_df.iloc[:, :-1]
Y_df = cancer_df.iloc[:, -1]

# 8:2 비율로 훈련/테스트 세트 분할!
X_train, X_test, y_train, y_test=train_test_split(X_df, Y_df, test_size=0.2, random_state=156 )

# 한 번 더 쪼개서 9:1 비율로 훈련/검증 세트 분할!
X_train, X_valid, y_train, y_valid= train_test_split(X_train, y_train, test_size=0.1, random_state=156 )

1) 하이퍼파라미터 탐색 공간 설정

  • Optuna는 objective 함수 내에 한 번에 설정했던 것과 달리, HyperOpt는 별도로 설정해줌
  • `hp.xxx` 함수로 각 하이퍼파라미터의 탐색 범위를 지정
    • `hp.quiform(label, low, high, q)` : label이라는 하이퍼파라미터를, low~high까지 q 간격으로 탐색
    • `hp.uniform(label, low, high)` : low~high까지 정규분포 형태의 탐색 공간 설정
    • `hp.randint(label, high)` : 0~high까지 random한 정수값으로 탐색 공간 설정
    • `hp.loguniform(label, low, high)` : exp(uniform(low, high))을 반환하고, 반환 값의 log 변환된 값이 정규분포 형태의 탐색 공간 설정
    • `hp.choice(label, options)` : 탐색 값에 문자열이 섞여있을 경우에 설정
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),
                   }

# max_depth : 5에서 20까지 1간격으로 검색
# min_child_weight : 1에서 2까지 1간격으로 검색
# colsample_bytree : 0.5에서 1까지 정규분포된 값으로 검색
# learning_rate는 0.01에서 0.2 사이 정규 분포된 값으로 검색

2) Objective 함수 설정

  • HyperOpt는 입력값과 반환값이 모두 실수형(float)이기 때문에 정수형(int) 하이퍼파라미터 입력 시 형변환이 필요
  • HyperOpt는 목적함수의 최솟값을 향해 최적화하는 방식이라, 클수록 좋은 성능 지표일 경우 -1을 곱해줘야 함
    • (MAE, RMSE 같은 경우에는 원래 작을수록 좋은 지표니까 안 곱해줘도 됨)
from sklearn.model_selection import cross_val_score
from xgboost import XGBClassifier
from hyperopt import STATUS_OK

#⚠️search_space에 입력된 값들은 다 실수형으로 반환되니까, max_depth 같은 정수형 hp는 변환을 해줘야 함!
#⚠️Accuracy는 높을수록 더 좋은 지표니까, (-1) 곱해서 성능 좋을수록 최소가 되도록 변환!

def objective_func(search_space):
    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=5) # 교차검증 가능
    
    # cv 개수만큼 나온 accuraccy를 평균내서 반환 & 높을수록 좋으니까 -1을 곱해줌
    return {'loss':-1 * np.mean(accuracy), 'status': STATUS_OK}

3) 최적화 실행

  • fmin() 함수 하나만 있으면 됨! 아래 인자들은 필요에 따라 설정하면 됨.
    • `fn` : 목적함수
    • `space` : 검색공간 딕셔너리
    • `algo` : Surrogate 알고리즘. 기본값은 tpe.suggest로 TPE를 의미함
    • `max_evals` : 최적 입력값을 찾기 위한 시도 횟수
    • `trials` : 최적 입력값을 찾기 위해 시도한 trial과 그때의 목적함수값 결과를 저장하는 데 사용됨. Trial 클래스를 객체 선언하고 여기다 집어넣음
    • `rstate` : 동일한 결과를 갖도록 설정하는 랜덤 시드값
from hyperopt import fmin, tpe, Trials

trial_val = Trials() # 먼저 Trial 객체 생성

# fmin 함수로 HP튜닝 실행
best = fmin(fn=objective_func, # 앞에서 설정한 목적함수와
            space=xgb_search_space, # 탐색 공간을 지정합니다.
            algo=tpe.suggest, # 목적함수 알고리즘은 TPE로 하고,
            max_evals=50, # 최대 반복 횟수는 50회로 지정합니다.
            trials=trial_val, # Trial 객체를 넣고
            rstate=np.random.default_rng(seed=123)) # 랜덤시드도 설정합니다.
# 튜닝 결과 출력
print('최적의 하이퍼파라미터 조합:', best)
  • 코드 실행 결과 (예시)

 

➕플러스알파

1) Optuna 세부 설정

  • 샘플러(Sampler): 하이퍼파라미터 조합을 선택하는 방식을 결정
# (1) TPE (Tree-structured Parzen Estimator) - 기본값, 가장 많이 사용되는 베이지안 기법
sampler = optuna.samplers.TPESampler(seed=42)

# (2) CMA-ES (진화 전략 기반 최적화) - 또 다른 베이지안 최적화 기법
sampler = optuna.samplers.CmaEsSampler()

# (2) Random Sampler - 무작위 탐색 기법
sampler = optuna.samplers.RandomSampler(seed=42)

# (3) Grid Sampler - 격자 탐색 기법
search_space = {
    'max_depth': [3, 5, 7, 9],
    'learning_rate': [0.01, 0.1, 0.3]
}
sampler = optuna.samplers.GridSampler(search_space)
  • 프루너(Pruner): 성능이 좋지 않은 시도를 조기에 중단시켜 계산 자원을 절약함
# (1) 중앙값 기반 가지치기
pruner = optuna.pruners.MedianPruner(
    n_startup_trials=5,     # 처음 5번의 시도는 가지치기하지 않음
    n_warmup_steps=0,       # 각 시도에서 웜업 스텝 수
    interval_steps=1        # 가지치기 평가 간격
)

# (2) HyperBand 프루너 - 리소스 할당 기반 가지치기
pruner = optuna.pruners.HyperbandPruner(
    min_resource=1,
    max_resource=10,
    reduction_factor=3
)

# (3) 성능 기준 가지치기
pruner = optuna.pruners.PercentilePruner(
    percentile=25.0,        # 하위 25% 성능의 시도는 중단
    n_startup_trials=5
)
  • 병렬 처리: `n_jobs`로 설정
# 병렬 처리 (ex. 4개의 프로세스로 병렬 실행)
study.optimize(objective, n_trials=100, n_jobs=4)
  • 평가지표 여러 개 최적화: `directions`에 리스트로 입력하여 여러 지표를 동시에 최적화할 수도 있음
# ex) 정확도는 높이고, 모델 복잡도는 낮추는 경우
study = optuna.create_study(directions=['maximize', 'minimize'])

def multi_objective(trial):
    params = {...}  # 하이퍼파라미터 조합 설정
    
    # 모델 훈련 및 평가
    accuracy = ...
    model_complexity = ...
    
    return accuracy, model_complexity # 평가할 지표를 여러 개 반환!
  • 기존 Study 이어서 하기: 처음에 `storage`로 결과를 저장해두고, `load_study`로 다시 불러와서 진행할 수 있음
# 이전에 저장한 study 불러오기
study = optuna.load_study(
    study_name='xgboost_optimization',
    storage='sqlite:///optuna_study.db'
)

# 추가로 50번 더 최적화 실행
study.optimize(objective, n_trials=50)

 

2) study.optimize()의 실행 흐름 (+ lambda 사용하는 이유)

  • optimize 함수의 사용 방식에 대해 헷갈리는 부분이 있어 정리해둔다. 내가 헷갈렸던 건 아래 세 경우이다.
    1. def objective(trial)로 정의 후, study.optimize(objective, n_trials=100) 사용
    2. def objective(trial, X, y)로 정의 후, study.optimize(lambda trial: objective(trial, X_train, y_train), n_trials=100) 사용
    3. def objective(trial, X, y)로 정의 후, study.optimize(objective(trial, X_train, y_train), n_trials=100) 사용
  • 1,2번은 가능⭕ 3번은 불가능❌
    • 원래 optimize 함수에는 함수 자체를 전달해줘야 하는데, 3번은 '함수가 실행된 결과값'을 전달하기 때문
  • ✅1번 경우 optimize 함수 실행 흐름
    1. Optuna 내부에서 자동으로 새로운 trial 객체를 만듦 (trial = Trial(study, trial_number))
    2. Optuna 내부에서 objective(trial)을 호출함
    3. objective(trial)이 실행되면서 trial.suggest_xxx() 함수들이 호출되고, 이 trial에서 선택된 하이퍼파라미터로 모델을 학습 후 점수 반환
    4. study 객체가 반환된 점수를 저장하고 다음 trial을 실행함. (이걸 n_trials에 지정한 횟수만큼 반복 실행)
      • 이때 이전 trial들의 결과를 바탕으로 sampler가 다음 하이퍼파라미터 조합을 결정함
      • 진행 중인 trial이 좋지 않은 결과를 보이면 pruner가 해당 Trial을 중단할지 결정함
  • ✅ 2번 경우 optimize 함수 실행 흐름
    1. Optuna 내부에서 자동으로 새로운 trial 객체를 만듦
    2. Optuna 내부에서 lambda trial: objective(trial, X_train, y_train)을 호출함
      • (여기만 1번과 다르고 나머지 과정은 다 동일함!!)  
      • 원래는 그냥 trial만 넣도록 되어있는데, X, y를 같이 넣어주고 싶은데 원래는 objective의 인자로 trial만 들어가니까 안됨 → 그래서 objective에 X, y까지 같이 입력하는 걸 lambda로 감싸서 trial 객체를 정상적으로 받을 수 있도록 한 것!
    3. lambda trial: objective(trial, X_train, y_train)이 실행되면서 trial.suggest_xxx() 함수들이 호출되고, 이 trial에서 선택된 하이퍼파라미터로 모델을 학습 후 점수 반환
    4. study 객체가 반환된 점수를 저장하고 다음 trial을 실행함.
👨🏻‍🏫 lambda 함수란?
- 이름 없는 간단한 일회용 함수로, lambda 인자: 표현식 형태로 사용함
- 예를 들어, add라는 함수를 lambda로 구현하면 아래와 같음

 

 

 

 

 

'ML & DL > 데이터마이닝' 카테고리의 다른 글

[데이터마이닝] eXplainable AI ② LIME, SHAP  (0) 2025.03.09
[데이터마이닝] eXplainable AI ① PDP, ICE, Feature Importance  (0) 2025.03.08
[데이터마이닝] 클러스터링 알고리즘  (0) 2025.03.06
[데이터마이닝] 앙상블 학습 (Ensemble learning)  (0) 2025.03.05
[데이터마이닝] 트리 모델 가지치기(Pruning)  (0) 2025.03.04
'ML & DL/데이터마이닝' 카테고리의 다른 글
  • [데이터마이닝] eXplainable AI ② LIME, SHAP
  • [데이터마이닝] eXplainable AI ① PDP, ICE, Feature Importance
  • [데이터마이닝] 클러스터링 알고리즘
  • [데이터마이닝] 앙상블 학습 (Ensemble learning)
simon919
simon919
개인적으로 공부한 내용을 기록하고 나누는 블로그입니다. 데이터 분석, 인공지능에 관한 내용을 주로 다룹니다.
  • simon919
    문과생의 AI 생존기
    simon919
  • 전체
    오늘
    어제
    • 분류 전체보기 (84)
      • ML & DL (38)
        • 머신러닝 기초 (23)
        • 딥러닝 기초 (6)
        • 데이터마이닝 (9)
      • Data structure & Algorithm (1)
      • SQL (21)
        • BigQuery (13)
        • MySQL (8)
      • Statistics (4)
        • 교육 연구를 위한 통계 (4)
        • Linear Algebra (0)
      • Python (17)
        • Pandas (16)
        • Matplotlib (0)
        • Numpy (0)
        • Web Crawling (1)
      • Projects (0)
      • Etc. (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 글자가 깨지면 새로고침 해주세요 :)
  • 인기 글

  • 태그

    해커랭크
    kmooc
    ml기초
    Xai
    SQL코딩테스트
    데이터마이닝
    티스토리 스킨
    BigQuery
    Conv2d
    블로그 스킨
    리트코드
    최우수혼공족
    SQL문제풀이
    SQL
    혼공학습단
    mysql
    kmeans
    특성맵 시각화
    통계학 기초
    교육통계
    Bayesian Optimization
    HELLO 스킨
    agglomerative
    Functional API
    silhouette index
    pytorch
    혼공머신
    google cloud
    MaxPooling2D
    pandas
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
simon919
Bayesian Optimization 코드 (Optuna, HyperOpt)
상단으로

티스토리툴바