하이퍼파라미터 튜닝
Hyperparameter Tuning
모델 학습에 영향 주는 설정값 최적화. Grid Search, Bayesian.
Hyperparameter Tuning
모델 학습에 영향 주는 설정값 최적화. Grid Search, Bayesian.
하이퍼파라미터 튜닝(Hyperparameter Tuning)은 머신러닝 모델의 최적 하이퍼파라미터 조합을 체계적으로 탐색하는 과정입니다. 학습률, 배치 크기, 레이어 수 같은 하이퍼파라미터는 모델 성능에 큰 영향을 미치지만, 최적값을 사전에 알 수 없습니다. 따라서 다양한 조합을 실험하고 검증 데이터 성능을 비교해 최적 설정을 찾아야 합니다. 이 과정을 자동화하는 것이 하이퍼파라미터 튜닝입니다.
대표적인 튜닝 방법으로 Grid Search, Random Search, Bayesian Optimization이 있습니다. Grid Search는 모든 조합을 시험하므로 확실하지만 차원이 늘면 기하급수적으로 비용이 증가합니다. Random Search는 무작위 샘플링으로 효율적이며, 실제로 많은 경우 Grid Search보다 좋은 결과를 더 빨리 찾습니다. Bayesian Optimization은 이전 실험 결과를 바탕으로 유망한 영역을 집중 탐색해 가장 효율적입니다.
2024-2025년 현재 Optuna, Ray Tune, Weights & Biases Sweeps가 주요 튜닝 도구입니다. Optuna는 Python 네이티브로 사용이 간편하고 TPE(Tree-structured Parzen Estimator), CMA-ES 같은 고급 알고리즘을 제공합니다. Ray Tune은 분산 튜닝에 강하고 다양한 스케줄러를 지원합니다. 최근에는 PBT(Population Based Training), ASHA(Asynchronous Successive Halving) 같은 조기 종료 기법이 대규모 튜닝의 비용을 획기적으로 줄이고 있습니다.
실무에서 효과적인 튜닝 전략은 단계적 접근입니다. 먼저 넓은 범위에서 Random Search로 유망한 영역을 찾고, 그 영역에서 Bayesian Optimization으로 정밀 탐색합니다. 자원이 제한적이면 조기 종료(pruning)를 적극 활용하세요. 또한 모든 실험을 로깅해 나중에 분석할 수 있게 하고, 하이퍼파라미터 간 상호작용(예: learning rate와 batch size)도 고려해야 합니다.
Optuna를 사용한 하이퍼파라미터 튜닝 예제입니다.
import optuna
from optuna.samplers import TPESampler
from optuna.pruners import MedianPruner
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
from sklearn.model_selection import train_test_split
import numpy as np
# 샘플 데이터 생성
def create_sample_data(n_samples=1000, n_features=20):
X = np.random.randn(n_samples, n_features).astype(np.float32)
y = (X[:, 0] + X[:, 1] * 2 > 0).astype(np.int64)
return train_test_split(X, y, test_size=0.2)
class TunableModel(nn.Module):
"""튜닝 가능한 하이퍼파라미터를 가진 모델"""
def __init__(self, input_dim, hidden_dim, num_layers, dropout):
super().__init__()
layers = []
in_dim = input_dim
for i in range(num_layers):
layers.append(nn.Linear(in_dim, hidden_dim))
layers.append(nn.ReLU())
layers.append(nn.Dropout(dropout))
in_dim = hidden_dim
layers.append(nn.Linear(hidden_dim, 2))
self.network = nn.Sequential(*layers)
def forward(self, x):
return self.network(x)
def objective(trial: optuna.Trial) -> float:
"""Optuna 목적 함수 - 최소화할 값 반환"""
# 하이퍼파라미터 샘플링
hidden_dim = trial.suggest_int("hidden_dim", 32, 256, step=32)
num_layers = trial.suggest_int("num_layers", 1, 4)
dropout = trial.suggest_float("dropout", 0.1, 0.5)
learning_rate = trial.suggest_float("learning_rate", 1e-5, 1e-2, log=True)
batch_size = trial.suggest_categorical("batch_size", [16, 32, 64, 128])
optimizer_name = trial.suggest_categorical("optimizer", ["Adam", "AdamW", "SGD"])
# 데이터 준비
X_train, X_val, y_train, y_val = create_sample_data()
train_dataset = TensorDataset(torch.tensor(X_train), torch.tensor(y_train))
val_dataset = TensorDataset(torch.tensor(X_val), torch.tensor(y_val))
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=batch_size)
# 모델 생성
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = TunableModel(
input_dim=20,
hidden_dim=hidden_dim,
num_layers=num_layers,
dropout=dropout
).to(device)
# 옵티마이저 설정
if optimizer_name == "Adam":
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
elif optimizer_name == "AdamW":
optimizer = optim.AdamW(model.parameters(), lr=learning_rate)
else:
optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=0.9)
criterion = nn.CrossEntropyLoss()
# 학습 루프
n_epochs = 30
for epoch in range(n_epochs):
model.train()
for batch_x, batch_y in train_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
optimizer.zero_grad()
output = model(batch_x)
loss = criterion(output, batch_y)
loss.backward()
optimizer.step()
# 검증
model.eval()
val_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for batch_x, batch_y in val_loader:
batch_x, batch_y = batch_x.to(device), batch_y.to(device)
output = model(batch_x)
val_loss += criterion(output, batch_y).item()
_, predicted = torch.max(output, 1)
total += batch_y.size(0)
correct += (predicted == batch_y).sum().item()
accuracy = correct / total
# 중간 결과 보고 (조기 종료 판단용)
trial.report(accuracy, epoch)
# 성능이 나쁘면 조기 종료 (pruning)
if trial.should_prune():
raise optuna.TrialPruned()
return accuracy # 최대화할 값
def run_hyperparameter_tuning():
"""하이퍼파라미터 튜닝 실행"""
# 스터디 생성 (TPE 샘플러 + Median Pruner)
study = optuna.create_study(
direction="maximize", # accuracy 최대화
sampler=TPESampler(seed=42),
pruner=MedianPruner(n_warmup_steps=5)
)
# 최적화 실행
study.optimize(
objective,
n_trials=50, # 총 50회 시도
timeout=600, # 최대 10분
show_progress_bar=True
)
# 결과 출력
print(f"\n{'='*50}")
print(f"Best trial:")
print(f" Value (Accuracy): {study.best_trial.value:.4f}")
print(f" Params:")
for key, value in study.best_trial.params.items():
print(f" {key}: {value}")
# 하이퍼파라미터 중요도 분석
print(f"\n하이퍼파라미터 중요도:")
importances = optuna.importance.get_param_importances(study)
for param, importance in importances.items():
print(f" {param}: {importance:.4f}")
return study
# 분산 튜닝 (Ray Tune 예시)
def ray_tune_example():
"""Ray Tune을 사용한 분산 하이퍼파라미터 튜닝"""
from ray import tune
from ray.tune.schedulers import ASHAScheduler
# 탐색 공간 정의
config = {
"hidden_dim": tune.choice([64, 128, 256]),
"num_layers": tune.randint(1, 5),
"learning_rate": tune.loguniform(1e-5, 1e-2),
"batch_size": tune.choice([32, 64, 128]),
}
# ASHA 스케줄러 (조기 종료)
scheduler = ASHAScheduler(
max_t=100,
grace_period=10,
reduction_factor=3
)
# 튜닝 실행
analysis = tune.run(
objective, # 동일한 목적 함수 사용 가능
config=config,
num_samples=100,
scheduler=scheduler,
resources_per_trial={"cpu": 2, "gpu": 0.5}
)
return analysis.best_config
if __name__ == "__main__":
study = run_hyperparameter_tuning()
# 최적 하이퍼파라미터로 최종 모델 학습
best_params = study.best_trial.params
print(f"\n최적 하이퍼파라미터로 최종 모델 학습...")
print(f"Best params: {best_params}")
| 방법 | 효율성 | 구현 복잡도 | 적합한 상황 |
|---|---|---|---|
| Grid Search | 낮음 | 쉬움 | 하이퍼파라미터 2-3개 |
| Random Search | 중간 | 쉬움 | 빠른 탐색, 넓은 범위 |
| Bayesian (TPE) | 높음 | 중간 | 비용이 큰 학습, 정밀 탐색 |
| ASHA/Hyperband | 매우 높음 | 중간 | 대규모 탐색, 조기 종료 |
| PBT | 높음 | 높음 | 분산 학습, 동적 튜닝 |
튜닝에 사용한 검증 세트로 최종 성능을 측정하면 과대평가됩니다. 튜닝용 검증 세트와 별도의 테스트 세트를 유지하거나 중첩 교차 검증을 사용하세요.
경험에 기반한 좁은 범위는 최적점을 놓칠 수 있습니다. 처음엔 넓은 범위(log scale)로 시작하고, 결과를 보고 범위를 좁혀가세요.
모든 실험에 동일한 random seed를 사용하고, 하이퍼파라미터와 결과를 자동 로깅하세요. Optuna DB 저장이나 W&B 연동으로 나중에 재현하고 분석할 수 있습니다.