Files
2026-07-13 13:22:34 +08:00

158 lines
5.3 KiB
Python

"""
Hyperparameter Optimization Example with Pure PyTorch and MLflow
This example demonstrates:
- Using MLflow to track hyperparameter optimization trials
- Parent/child run structure for organizing HPO experiments
- Pure PyTorch training (no Lightning dependencies)
- Simple MNIST classification with configurable hyperparameters
Run with: python hpo_mnist.py --n-trials 5 --max-epochs 3
"""
import argparse
import optuna
import torch
import torch.nn.functional as F
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import mlflow
class SimpleNet(nn.Module):
def __init__(self, hidden_size, dropout_rate):
super().__init__()
self.fc1 = nn.Linear(784, hidden_size)
self.dropout = nn.Dropout(dropout_rate)
self.fc2 = nn.Linear(hidden_size, 10)
def forward(self, x):
x = x.view(-1, 784)
x = F.relu(self.fc1(x))
x = self.dropout(x)
x = self.fc2(x)
return F.log_softmax(x, dim=1)
def train_epoch(model, device, train_loader, optimizer):
model.train()
for data, target in train_loader:
data = data.to(device)
target = target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
def evaluate(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data = data.to(device)
target = target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction="sum").item()
pred = output.argmax(dim=1, keepdim=True)
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
accuracy = correct / len(test_loader.dataset)
return test_loss, accuracy
def objective(trial, args, train_loader, test_loader, device):
# Suggest hyperparameters
lr = trial.suggest_float("lr", 1e-4, 1e-1, log=True)
hidden_size = trial.suggest_int("hidden_size", 64, 512, step=64)
dropout_rate = trial.suggest_float("dropout_rate", 0.1, 0.5)
batch_size = trial.suggest_categorical("batch_size", [32, 64, 128])
# Recreate data loaders with new batch size
train_loader = DataLoader(train_loader.dataset, batch_size=batch_size, shuffle=True)
test_loader = DataLoader(test_loader.dataset, batch_size=batch_size, shuffle=False)
# Start nested MLflow run for this trial
with mlflow.start_run(nested=True, run_name=f"trial_{trial.number}"):
# Log hyperparameters
mlflow.log_params({
"lr": lr,
"hidden_size": hidden_size,
"dropout_rate": dropout_rate,
"batch_size": batch_size,
})
# Create model and optimizer
model = SimpleNet(hidden_size, dropout_rate).to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
# Training loop
for epoch in range(args.max_epochs):
train_epoch(model, device, train_loader, optimizer)
test_loss, accuracy = evaluate(model, device, test_loader)
# Log metrics for each epoch
mlflow.log_metrics({"test_loss": test_loss, "accuracy": accuracy}, step=epoch)
# Return final accuracy for optimization
return accuracy
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--n-trials", type=int, default=10, help="Number of HPO trials")
parser.add_argument("--max-epochs", type=int, default=5, help="Epochs per trial")
parser.add_argument("--batch-size", type=int, default=64, help="Initial batch size")
args = parser.parse_args()
# Setup device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Load MNIST data
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,)),
])
train_dataset = datasets.MNIST("./data", train=True, download=True, transform=transform)
test_dataset = datasets.MNIST("./data", train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=args.batch_size, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=args.batch_size, shuffle=False)
# Start parent MLflow run
with mlflow.start_run(run_name="HPO_Parent"):
mlflow.log_params({"n_trials": args.n_trials, "max_epochs": args.max_epochs})
# Create Optuna study
study = optuna.create_study(direction="maximize", study_name="mnist_hpo")
# Run optimization
study.optimize(
lambda trial: objective(trial, args, train_loader, test_loader, device),
n_trials=args.n_trials,
)
# Log best results to parent run
mlflow.log_metrics({
"best_accuracy": study.best_value,
"best_trial": study.best_trial.number,
})
# Log best hyperparameters with 'best_' prefix to avoid conflicts
best_params = {f"best_{k}": v for k, v in study.best_params.items()}
mlflow.log_params(best_params)
print(f"\nBest trial: {study.best_trial.number}")
print(f"Best accuracy: {study.best_value:.4f}")
print(f"Best params: {study.best_params}")
if __name__ == "__main__":
main()