하이퍼파라미터 튜닝 전략: Grid Search·Random Search·Bayesian Optimization
Grid Search, Random Search, Bayesian Optimization의 탐색 방식과 검증·예산·재현성 관리 기준을 비교해 머신러닝 튜닝 전략을 정리한다.
2026-08-15 · 최초 발행 2024-04-29
모델 학습 바깥에서 결정되는 값을 어떻게 탐색할 것인가
하이퍼파라미터는 학습 알고리즘이 데이터에서 직접 추정하지 않는 외부 제어 변수다. 학습 전에 사용자가 정하는 설정값으로, 정규화 강도, 트리 깊이, 러닝레이트가 여기에 속한다. 튜닝의 대상은 이 설정값 λ에 대해 교차 검증 성능 지표 f(λ)를 최대화하거나 최소화하는 목적 함수이며, 교차 검증과 스코어링 메트릭으로 평가 기준을 일정하게 맞춘다.
분류에서는 AUC나 F1, 회귀에서는 RMSE나 MAE처럼 문제에 맞는 지표를 정하고, 그 지표를 신뢰할 수 있도록 데이터 분할 방식까지 함께 결정해야 한다. 탐색 공간에는 연속형·이산형·범주형 변수의 범위와 분포를 명시한다. log-uniform이나 categorical 같은 분포 선택도 이 단계에 포함된다. 변수 간 상호 의존성이나 합계=1 같은 제약이 있다면 함께 모델링해야 한다.
대표적인 탐색 방식은 다음과 같다.
- Grid Search는 후보값으로 구성한 격자를 모두 평가한다.
- Random Search는 사전에 정의한 분포에서 무작위 표본을 뽑아 평가한다.
- Bayesian Optimization은 GP, TPE, RF 같은 대리 모델로 목적 함수를 근사하고, 획득 함수로 탐색과 활용의 균형을 조절한다.
탐색 공간과 검증 조건을 먼저 고정한다
변수 유형에 맞춰 log, linear, categorical 스케일을 선택한다. learning_rate∈[1e-4, 1e-1]처럼 값의 차이가 큰 경우 로그 스케일을 적용할 수 있다. 중요한 변수에는 더 넓은 범위를 주고, 덜 중요한 변수는 제한된 범위로 다룰 수 있다. 부스팅 유형에 따라 하위 파라미터가 달라지는 조건부 공간도 설계 대상이며, penalty='l1'일 때의 solver 제약처럼 조합 자체가 유효하지 않은 조건을 탐색 공간에 반영해야 한다.
시도 횟수와 시간 제한, early stop이나 pruner 같은 조기 종료 규칙도 탐색 전에 정한다. 최근 N회 동안 개선이 없거나 임계 향상폭에 미치지 못하는 경우, 개선 폭이 기준에 미달하는 횟수 k, 비용 제약처럼 수렴을 판단할 기준이 필요하다. 실행 예산은 n_iter, timeout, early stopping으로 정하고, 실패한 시도를 어떻게 재시도할지도 미리 정의한다.
평가의 일관성은 교차 검증 전략과 스코어 정의에서 시작된다. 일반적인 데이터에는 K-Fold, 클래스 비율을 유지해야 하는 분류에는 StratifiedKFold, 그룹 구분이 필요한 데이터에는 GroupKFold, 시간 순서가 중요한 데이터에는 TimeSeriesSplit을 선택한다. 분류에서 AUC/F1, 회귀에서 RMSE/MAE를 쓰는 식으로 평가 지표를 일관되게 유지하되, 비즈니스 목적과 비용 민감도도 반영한다.
재현성은 탐색 결과의 부수 조건이 아니다. random_state, 데이터 분할 시드, 난수 발생기, 라이브러리 버전을 고정한다. 동시 실행 환경에서도 재현성을 관리해야 동일한 조건에서 결과를 비교할 수 있다. 멀티 워커 실행이 필요하면 n_jobs와 Optuna RDB 같은 분산 스토리지를 활용할 수 있다.
검증 데이터에 맞추어 탐색을 반복하면 과탐색과 과적합이 생길 수 있다. 검증 누수를 차단하고 nested CV를 고려하며, 탐색 범위를 완충하는 방식으로 이를 관리한다. 무효 파라미터나 학습 실패는 예외로 포착해 trial을 무효 처리하고, 로그와 메트릭을 남겨 원인을 추적한다.
탐색 방식이 달라지면 운영상의 선택도 달라진다
| 방법 | 성능(최적값 접근) | 확장성(고차원/연속) | 일관성(재현성) | 안정성(노이즈 내성) | 운영 편의(설정/병렬) |
|---|---|---|---|---|---|
| Grid Search | 작은 공간에서 우수, 고해상도 필요 시 비용 급증 | 낮음, 차원의 저주 영향 큼 | 매우 높음, 결정적 탐색 | CV로 완화 가능하나 경계 민감 | 매우 쉬움, 병렬화 용이 |
| Random Search | 중요 변수에 빠르게 접근, anytime 성질 | 중~높음, 고차원 적합 | 중간, 시드 의존 | 무작위 표본화로 비교적 강함 | 쉬움, 예산만 정하면 됨 |
| Bayesian Opt. | 표본 효율 높음, 고비용 모델에 유리 | 중간, ~20-50차원 실무 적정 | 중~높음, 초기점·시드 영향 | 대리모델·노이즈 고려로 강함 | 중~어려움, 설정/병렬 제약 존재 |
Grid Search는 가능한 조합을 전수 탐색하므로 작은 공간에서 후보를 빠짐없이 비교할 수 있다. 저차원이고 변수 간 상호작용이 약한 공간에서 사용하기 좋지만, 해상도를 높이거나 차원이 늘면 비용이 급격히 커진다.
Random Search는 정의한 분포에서 확률적으로 조합을 뽑는다. 중요 변수에 빠르게 도달할 수 있고 탐색을 언제든 멈출 수 있는 성질이 있어 넓고 고차원인 탐색 공간에서 효율적이다. 다만 결과 품질은 분포를 어떻게 설계했는지에 영향을 받는다.
Bayesian Optimization은 이전 평가 결과를 바탕으로 사후분포를 갱신하고 획득함수로 다음 시도를 정한다. 평가 자체가 비싼 모델에서 표본 효율이 높은 대신, 대리 모델과 획득함수 설정, 탐색과 활용의 균형, 병렬 실행 제약을 함께 고려해야 한다.
입력부터 예외 처리까지 이어지는 튜닝 흐름
이 흐름은 입력·처리·출력의 경계를 분명히 한다. 문제 정의에서 지표와 분할을 정하고, 탐색 공간과 전략을 설계한 뒤 예산·병렬 설정, 실행·로깅, 재현성 검증과 모델 등록으로 이어진다. 분할과 시드를 고정한 뒤 각 시도를 로그와 체크포인트로 남기고, 예산 소진이나 수렴 여부에 따라 다음 탐색을 결정한다.
잘못된 설정은 해당 시도를 건너뛰거나 패널티 스코어를 부여할 수 있다. 무효 파라미터나 학습 실패는 예외로 포착해 trial을 무효 처리하고, Timeout이나 OOM에는 배치 또는 트리 수를 줄이고 조기 중단으로 대응한다.
데이터와 예산에 맞춘 적용 장면
탭уляр 데이터 분류와 회귀
XGBoost, LightGBM, RandomForest 같은 트리 기반 모델에는 Random Search 또는 Bayesian Optimization을 적용할 수 있다. learning_rate, max_depth, n_estimators를 log 또는 정수 스케일로 탐색하며, 예산은 100~300 시도를 권장한다.
불균형 분류에서는 StratifiedKFold로 분할하고 AUC/F1을 최적화 대상으로 둔다. class_weight와 threshold도 탐색 대상에 포함할 수 있다. 넓은 범위는 Random Search로 훑고, 이후 Bayesian 방식으로 미세 조정하는 흐름을 선택할 수 있다.
회귀에서는 시계열이라면 TimeSeriesSplit, 그렇지 않다면 K-Fold를 적용하고 RMSE를 기준으로 둔다. 학습률은 로그 스케일을 고려한다. 평가가 비싼 앙상블 모델은 Bayesian Optimization, 가벼운 모델은 Random Search가 맞는 경우가 있다.
학습 비용이 큰 딥러닝 모델
딥러닝에서는 Bayesian 방식에 ASHA나 Hyperband 같은 다중 충실도 방식을 조합할 수 있다. 배치 크기, 러닝레이트, weight decay를 중심으로 탐색하고, 조기 종료와 프루너로 불필요한 실행을 줄인다. 예산 기반 Random 초기화 뒤 Scheduler와 optimizer 조합을 Bayesian 방식으로 보강할 수 있으며, 에폭과 조기 종료 정책을 명시하고 하드웨어 예산을 함께 관리한다.
시간이 제한된 MLOps 환경
시간 예산이 정해진 환경에서는 Random 탐색 뒤 Bayesian warm-start로 이어지는 anytime 전략을 운영할 수 있다. MLflow나 Weights & Biases로 실험을 추적하고, 체크포인트와 재시도 정책을 포함한다.
scikit-learn에서 격자 탐색과 무작위 탐색 비교하기
전제조건: Python 3.10+, scikit-learn >= 1.4, scipy 설치, CPU 환경 가정.
# pip install scikit-learn scipy
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV, StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from scipy.stats import randint, loguniform
import numpy as np
X, y = load_breast_cancer(return_X_y=True)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# Grid Search
grid = {
"n_estimators": [100, 200, 300],
"max_depth": [None, 5, 10],
"min_samples_split": [2, 5]
}
rf = RandomForestClassifier(random_state=42, n_jobs=-1)
gs = GridSearchCV(
rf, grid, cv=cv, scoring="accuracy", n_jobs=-1, verbose=0
)
gs.fit(X, y)
print("GridSearch best:", gs.best_params_, gs.best_score_)
# Random Search
dist = {
"n_estimators": randint(50, 500),
"max_depth": randint(3, 20),
"min_samples_split": randint(2, 10),
"max_features": ["sqrt", "log2", None]
}
rs = RandomizedSearchCV(
rf, dist, n_iter=50, cv=cv, scoring="accuracy", n_jobs=-1,
random_state=42, verbose=0
)
rs.fit(X, y)
print("RandomSearch best:", rs.best_params_, rs.best_score_)
소규모·저차원 공간에서는 Grid Search가 유리하고, 고차원 또는 연속 변수가 포함된 공간에서는 Random Search가 유리하다.
예시의 전제조건은 다음과 같다.
- Python 3.10+, scikit-learn >= 1.2, optuna >= 3.0
- 예시 데이터: sklearn.datasets 사용
Grid Search로 조합을 모두 평가하기
# pip install scikit-learn
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import GridSearchCV, StratifiedKFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import make_scorer, roc_auc_score
X, y = load_breast_cancer(return_X_y=True)
param_grid = {
"n_estimators": [200, 400, 800],
"max_depth": [None, 6, 10],
"min_samples_split": [2, 5, 10]
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scorer = make_scorer(roc_auc_score, needs_threshold=True)
gs = GridSearchCV(
RandomForestClassifier(random_state=42, n_jobs=-1),
param_grid=param_grid,
scoring=scorer,
cv=cv,
n_jobs=-1,
verbose=1
)
gs.fit(X, y)
print(gs.best_params_, gs.best_score_)
Random Search에서 분포를 지정하기
# pip install scikit-learn scipy
import numpy as np
from scipy.stats import randint, loguniform
from sklearn.model_selection import RandomizedSearchCV
param_distributions = {
"n_estimators": randint(200, 1200),
"max_depth": randint(3, 20),
"min_samples_split": randint(2, 20),
"min_samples_leaf": randint(1, 10),
"max_features": ["sqrt", "log2", None]
}
rs = RandomizedSearchCV(
RandomForestClassifier(random_state=42, n_jobs=-1),
param_distributions=param_distributions,
n_iter=60, # 예산 기반
scoring=scorer,
cv=cv,
random_state=42,
n_jobs=-1,
verbose=1
)
rs.fit(X, y)
print(rs.best_params_, rs.best_score_)
Optuna로 Bayesian Optimization 실행하기
전제조건: Python 3.10+, scikit-learn >= 1.4, optuna >= 3.0.
# pip install optuna scikit-learn
import optuna
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import cross_val_score, StratifiedKFold
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
import numpy as np
X, y = load_breast_cancer(return_X_y=True)
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
def objective(trial: optuna.Trial) -> float:
# 탐색 공간 정의 (log-scale 권장)
C = trial.suggest_float("C", 1e-3, 1e3, log=True)
gamma = trial.suggest_float("gamma", 1e-4, 1e0, log=True)
model = Pipeline([
("scaler", StandardScaler()),
("svc", SVC(kernel="rbf", C=C, gamma=gamma, random_state=42))
])
try:
scores = cross_val_score(model, X, y, cv=cv, scoring="accuracy", n_jobs=-1)
return float(np.mean(scores))
except Exception as e:
# 실패한 설정은 낮은 점수 반환 또는 pruner로 중단
return 0.0
sampler = optuna.samplers.TPESampler(seed=42) # Bayesian(TPE)
pruner = optuna.pruners.MedianPruner(n_startup_trials=10)
study = optuna.create_study(direction="maximize", sampler=sampler, pruner=pruner)
study.optimize(objective, n_trials=80, timeout=300) # 예산/시간 제한
print("Best value:", study.best_value)
print("Best params:", study.best_params)
초기 탐색 비율은 예를 들어 10~20%를 확보한 뒤 TPE 탐색을 강화할 수 있다. 분산 노이즈가 큰 데이터에서는 반복 CV 또는 평균화로 안정성을 높인다.
Optuna로 순차 탐색하기
# pip install optuna scikit-learn
import optuna
from sklearn.model_selection import cross_val_score
def objective(trial):
params = {
"n_estimators": trial.suggest_int("n_estimators", 300, 1200),
"max_depth": trial.suggest_int("max_depth", 3, 20),
"min_samples_split": trial.suggest_int("min_samples_split", 2, 20),
"min_samples_leaf": trial.suggest_int("min_samples_leaf", 1, 10),
"max_features": trial.suggest_categorical("max_features", ["sqrt", "log2", None]),
"random_state": 42,
"n_jobs": -1
}
model = RandomForestClassifier(**params)
scores = cross_val_score(model, X, y, cv=cv, scoring=scorer, n_jobs=-1)
return scores.mean()
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=80, timeout=600) # 시간 또는 횟수 예산
print(study.best_params, study.best_value)
Optuna를 분산 실행할 때는 RDB 스토리지로 create_study(storage="sqlite:///study.db", study_name="rf", load_if_exists=True)를 사용할 수 있다. 무효 시도는 trial.raise_if_fail=False 설정 또는 예외 포착 후 return float("nan")로 무효화한다.
선택 기준은 최적화 성능만이 아니다
변수가 1~2개이고 후보가 제한적이면 Grid Search가 적합하다. 고차원·연속 변수 중심으로 빠른 베이스라인이 필요하면 Random Search를 선택할 수 있다. 평가 비용이 큰 모델에서 제한된 예산으로 높은 성능을 노린다면 Bayesian Optimization이 맞는다.
운영 환경에서는 데이터 분할과 시드를 고정하고 라이브러리의 결정론 옵션을 사용해 재현성을 관리한다. Random 방식은 선형 병렬화가 쉽고, Bayesian 방식은 batch= qEI 등의 배치 획득으로 병렬화를 구현한다. 관측 노이즈에는 반복 측정이나 TPE/RF surrogate 같은 견고한 대리 모델을 고려한다.
실패와 타임아웃 처리 정책, MLflow 등의 로그·메트릭 추적, 중간 결과 저장도 절차에 포함한다. 합계=1 같은 제약과 하이퍼파라미터 간 상호 의존 관계는 유효성 검사를 거쳐야 한다.
베이스라인 대비 정확도·정밀도는 315%p 개선 가능성이 있으며, 복잡 모델에서는 상대적 개선 1030%를 기대할 수 있다. 베이스라인과 비교해 AUC/F1이 210%p 개선될 수 있으며, 고비용 모델에서는 Bayesian Optimization으로 동일 예산 대비 2040% 상대 성능 개선을 기대할 수 있다. Bayesian 기준으로 동등 성능에 도달하는 시도 횟수는 210배 줄고, 시간과 컴퓨팅 비용은 3070% 절감 가능하다. Random Search나 Bayesian Optimization을 도입하면 Grid Search 대비 평가 횟수를 50~80% 절감할 수 있다.
엄격한 데이터 분할과 시드 관리는 변동성을 20~50% 줄이고, 배포 후 성능 드리프트를 더 이르게 감지하는 데도 도움이 된다. 재현성과 추적성을 확보하면 실험 낭비를 줄이고 팀 간 공유와 검증도 쉬워진다. 탐색 메커니즘의 차이를 이해하는 것만으로는 충분하지 않으며, 검증·로그·중지 기준까지 일관되게 절차화해야 성능, 비용, 재현성을 함께 다룰 수 있다.