
지난 1편에서 GridSearch / RandomizedSearch의 한계(경우의 수 폭증, 시간/자원 문제)와 이를 보완하는 Bayesian Optimization(베이지안 최적화) 개념을 정리했습니다. 이번 2편에서는 실제로 HyperOpt를 사용해 XGBoost(XGBClassifier) 하이퍼파라미터를 튜닝하는 전체 흐름을 실습합니다.
이번 실습의 핵심은 아래 3단계입니다.
- Search Space(탐색 공간) 정의: 어떤 하이퍼파라미터를 어떤 범위에서 탐색할지 결정합니다.
- Objective Function(목적 함수) 정의: 입력된 파라미터로 모델을 학습/검증하여 “좋음/나쁨”을 수치로 반환합니다.
- **fmin()으로 최소 손실(loss)**을 찾기: HyperOpt가 반복적으로 시도하며 최적 파라미터를 찾아줍니다.
중요한 포인트: HyperOpt는 기본적으로 “loss를 최소화”하는 방향으로 움직입니다.
따라서 정확도(accuracy)를 최대화하고 싶다면 loss = -accuracy처럼 부호를 바꿔서 반환하는 패턴을 많이 씁니다.
아래 코드는 사용자가 주신 코드를 오류 가능 포인트를 보완하고, 각 단계 목적을 명확히 설명하도록 주석을 강화한 버전입니다.
(입문자 기준으로 “왜 이걸 하는지”까지 적었습니다.)
# =========================================================
# 0) 실습 준비: 데이터 로딩 및 Train/Validation/Test 분할
# =========================================================
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')
# (0-1) 실습용 데이터 로딩: 유방암(Breast Cancer) 이진 분류 데이터
# - target: 0/1 (악성/양성 등)
dataset = load_breast_cancer()
# (0-2) DataFrame 구성: feature와 target을 한 테이블로 관리하면 EDA/디버깅이 편합니다.
cancer_df = pd.DataFrame(
data=dataset.data,
columns=dataset.feature_names
)
cancer_df['target'] = dataset.target
# (0-3) 피처(X) / 타겟(y) 분리
X_features = cancer_df.iloc[:, :-1]
y_label = cancer_df.iloc[:, -1]
# (0-4) Train/Test 분할
# - Test는 "최종 성능 평가용"입니다. 튜닝 과정에서는 가능한 손대지 않는 것이 원칙입니다.
X_train, X_test, y_train, y_test = train_test_split(
X_features, y_label,
test_size=0.2,
random_state=156,
stratify=y_label # 클래스 비율 유지(불균형일 때 특히 중요)
)
# (0-5) Train을 다시 Train/Validation으로 분할
# - Validation은 Early Stopping 또는 튜닝 후 최종 모델 학습 과정에서 참고용으로 사용합니다.
X_tr, X_val, y_tr, y_val = train_test_split(
X_train, y_train,
test_size=0.1,
random_state=156,
stratify=y_train
)
# =========================================================
# 1) HyperOpt 1단계: Search Space(탐색 공간) 정의
# =========================================================
from hyperopt import hp
# HyperOpt는 "이 범위 안에서 어떤 값을 선택할지"를 먼저 알아야 합니다.
# 여기서 정의된 값들이 objective_func로 들어가며, HyperOpt가 반복적으로 샘플링합니다.
#
# - hp.quniform: 일정 간격(step)으로 뽑는 균일 분포 (결과는 float 형태로 반환되므로 int 변환 필요)
# - hp.uniform: 연속형 실수 값 범위에서 균일 샘플링
xgb_search_space = {
# 트리 깊이: 깊어질수록 복잡한 패턴을 학습하지만 과적합 위험도 증가
'max_depth': hp.quniform('max_depth', 5, 20, 1),
# 자식 노드가 되기 위한 최소 가중치 합
# 값이 커지면 분할이 보수적으로 이뤄져 과적합 방지에 도움
'min_child_weight': hp.quniform('min_child_weight', 1, 6, 1),
# 학습률: 한 번에 얼마나 크게 업데이트할지
# 너무 크면 과하게 학습(불안정), 너무 작으면 학습이 느림
'learning_rate': hp.uniform('learning_rate', 0.01, 0.2),
# 트리를 만들 때 사용하는 피처 비율
# 일부만 랜덤하게 사용 → 과적합 완화, 다양성 확보(앙상블 효과 강화)
'colsample_bytree': hp.uniform('colsample_bytree', 0.5, 1.0)
}
# =========================================================
# 2) HyperOpt 2단계: Objective Function(목적 함수) 정의
# =========================================================
from sklearn.model_selection import cross_val_score
from xgboost import XGBClassifier
from hyperopt import STATUS_OK
# 목적 함수는 HyperOpt가 "이 파라미터 조합이 얼마나 좋은지" 판단하는 기준입니다.
# 여기서는 cv=3 교차검증 평균 accuracy를 구하고,
# HyperOpt가 최소화(minimize)하도록 loss = -accuracy로 반환합니다.
def objective_func(search_space):
# HyperOpt에서 넘어오는 값은 모두 float 입니다.
# XGBClassifier는 max_depth, min_child_weight가 정수여야 하므로 int로 변환합니다.
max_depth = int(search_space['max_depth'])
min_child_weight = int(search_space['min_child_weight'])
# (중요) 실습/튜닝 속도
# - n_estimators가 크면 성능은 좋아질 수 있으나, 튜닝 시간이 급격히 늘어납니다.
# - 튜닝 단계에서는 시간을 아끼기 위해 적정 수준으로 제한하는 경우가 많습니다.
xgb_clf = XGBClassifier(
n_estimators=400,
max_depth=max_depth,
min_child_weight=min_child_weight,
learning_rate=search_space['learning_rate'],
colsample_bytree=search_space['colsample_bytree'],
eval_metric='logloss', # 이진 분류에서 일반적으로 사용되는 로그손실
random_state=156,
n_jobs=-1 # CPU 병렬 처리로 속도 개선
)
# 교차 검증으로 성능 추정
# - 여기서는 Train 데이터(X_train, y_train)만 사용합니다.
# - Test 데이터는 최종 평가에서만 사용해야 "진짜 일반화 성능"을 알 수 있습니다.
accuracy = cross_val_score(
xgb_clf,
X_train,
y_train,
scoring='accuracy',
cv=3
).mean()
# HyperOpt는 loss를 최소화하므로 -accuracy로 반환합니다.
return {
'loss': -accuracy,
'status': STATUS_OK
}
# =========================================================
# 3) HyperOpt 3단계: fmin()으로 최적 파라미터 탐색 실행
# =========================================================
from hyperopt import fmin, tpe, Trials
# Trials는 "각 시도(trial)의 파라미터와 결과(loss)"를 모두 저장해주는 로그 객체입니다.
# 나중에 어떤 값들이 시도되었는지 분석할 수 있어 매우 유용합니다.
trial_val = Trials()
# fmin: 주어진 objective_func를 여러 번 실행하면서 loss가 최소가 되는 파라미터를 탐색합니다.
# algo=tpe.suggest: Bayesian Optimization 계열(TPE) 알고리즘을 사용
best = fmin(
fn=objective_func,
space=xgb_search_space,
algo=tpe.suggest,
max_evals=50, # 총 50번 시도(=50 trials)
trials=trial_val,
rstate=np.random.default_rng(seed=9)
)
print(f"best : {best}")
print(
f"colsample_bytree : {best['colsample_bytree']}\n"
f"learning_rate : {best['learning_rate']}\n"
f"max_depth : {best['max_depth']}\n"
f"min_child_weight : {best['min_child_weight']}"
)


# =========================================================
# 4) 찾은 최적 파라미터로 최종 모델 학습 & 테스트 평가
# =========================================================
from sklearn.metrics import confusion_matrix, accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
def get_clf_eval(y_test, pred=None, pred_proba=None):
"""
분류 성능을 한 번에 확인하기 위한 평가 함수
- confusion matrix: 어떤 오류가 났는지 유형별로 확인
- accuracy: 전체 중 맞춘 비율
- precision/recall: Positive(1) 예측 성능을 상세히 확인
- f1: precision과 recall의 균형 지표
- roc_auc: threshold에 덜 민감한 전반적 분리 성능(확률 기반)
"""
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_score(y_test, pred_proba)
print("오차행렬")
print(confusion)
print(f"정확도(Accuracy) : {accuracy:.4f}")
print(f"정밀도(Precision): {precision:.4f}")
print(f"재현율(Recall) : {recall:.4f}")
print(f"F1 Score : {f1:.4f}")
print(f"ROC-AUC : {roc_auc:.4f}")
# HyperOpt 결과(best)는 float 형태로 들어올 수 있어 정리해서 넣습니다.
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),
eval_metric='logloss',
random_state=156,
n_jobs=-1
)
# eval_set:
# - 학습 데이터(X_tr, y_tr)와 검증 데이터(X_val, y_val) 성능을 학습 중 확인할 수 있습니다.
# - Early Stopping을 쓰면 "검증 성능이 더 이상 좋아지지 않을 때" 학습을 멈춰 과적합을 줄입니다.
evals = [(X_tr, y_tr), (X_val, y_val)]
# (주의) scikit-learn wrapper의 early stopping은 xgboost 버전/설치 상태에 따라 방식이 달라질 수 있습니다.
# 아래는 기본 학습 + 학습 과정 로그 출력 정도만 두고 진행합니다.
xgb_wrapper.fit(
X_tr,
y_tr,
eval_set=evals,
verbose=True
)
# 테스트 데이터 예측
preds = xgb_wrapper.predict(X_test)
# ROC-AUC 계산 등 확률 기반 지표를 위해 predict_proba 사용
pred_proba = xgb_wrapper.predict_proba(X_test)[:, 1]
# 평가 출력
get_clf_eval(y_test, preds, pred_proba)

1) 왜 Search Space가 중요한가요?
HyperOpt는 “아무 값이나” 실험하지 않습니다.
우리가 정한 탐색 범위 안에서만 최적화를 수행합니다.
- 범위가 너무 좁으면: 좋은 답이 바깥에 있어도 못 찾습니다.
- 범위가 너무 넓으면: 쓸데없는 시도(비효율)로 시간이 늘어납니다.
따라서 업무/경험 기반으로 현실적인 범위를 잡는 것이 튜닝의 절반입니다.
2) Objective Function은 “모델 성능을 숫자로 바꾸는 함수”입니다
Objective function은 HyperOpt 입장에서 “블랙박스”입니다.
- 입력: 하이퍼파라미터 조합
- 출력: loss 값(작을수록 좋음)
이번 글에서는 accuracy를 최대화하고 싶기 때문에
loss = -accuracy로 바꿔서 최소화 문제로 만든 것입니다.
3) 왜 교차검증(cross_val_score)을 쓰나요?
한 번만 Train/Validation으로 나누면 분할 운이 성능을 좌우할 수 있습니다.
교차검증은 여러 번 나눠 평균을 내기 때문에 좀 더 안정적인 성능 추정이 됩니다.
- 장점: 성능 추정이 안정적
- 단점: 반복 학습을 여러 번 하므로 시간이 늘어남
4) 왜 튜닝 후에는 Test로 “최종 평가”를 하나요?
튜닝 과정에서 계속 성능을 보고 파라미터를 고르면
그 데이터에 “간접적으로 맞춰지게” 됩니다(튜닝도 일종의 학습).
그래서 마지막에만 Test를 한 번 보고 “진짜 성능”을 확인합니다.
HyperOpt를 이용하면 GridSearch처럼 모든 조합을 다 시도하지 않아도,
**상대적으로 적은 시도(max_evals)**로도 좋은 하이퍼파라미터를 찾을 수 있습니다.
이번 글에서 기억할 구조는 딱 3가지입니다.
- Search Space: 어디를 탐색할지 정한다
- Objective Function: 무엇을 잘했다고 볼지 정의한다
- fmin(TPE): 최소 loss를 찾아 반복 시도한다
다음 글(3편)에서는 “왜 이런 값이 나왔는지”를 더 실전적으로 해석하기 위해
Trials 로그를 DataFrame으로 뽑아 시도 기록을 분석하거나,
early_stopping을 안정적으로 적용하는 패턴까지 연결하면 흐름이 아주 좋습니다.
'Programming' 카테고리의 다른 글
| IQR과 SMOTE 이해하기 (1) | 2025.12.26 |
|---|---|
| Customer Satisfaction 예측 프로젝트 (XGBoost/LightGBM + HyperOpt 튜닝) (0) | 2025.12.26 |
| (Bayesian Optimization 1편) GridSearch · RandomSearch · Bayesian Optimization 개념 완전 정리 (1) | 2025.12.22 |
| 앙상블 학습 3편: 부스팅(Boosting) · GBM · XGBoost · LightGBM 완전 정리 (0) | 2025.12.20 |
| 앙상블 학습 2편: 배깅(Bagging)과 랜덤 포레스트(Random Forest) 완전 이해 (0) | 2025.12.17 |