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