omyfish / src /train.py
fenghebonjour's picture
Upload folder using huggingface_hub
46bcc01 verified
Raw
History Blame Contribute Delete
5.03 kB
import argparse
import json
import time
from pathlib import Path
import torch
import torch.nn as nn
import yaml
from torch.optim import AdamW
from torch.optim.lr_scheduler import CosineAnnealingLR
from tqdm import tqdm
from src.dataset import build_dataloaders
from src.model import build_model
from src.utils import seed_everything
def _top5(logits: torch.Tensor, labels: torch.Tensor) -> float:
k = min(5, logits.size(1))
_, top = logits.topk(k, dim=1)
return top.eq(labels.view(-1, 1).expand_as(top)).any(1).float().mean().item()
class EarlyStopping:
def __init__(self, patience: int):
self.patience, self.best, self.count = patience, float("inf"), 0
def __call__(self, val_loss: float) -> bool:
if val_loss < self.best - 1e-4:
self.best, self.count = val_loss, 0
else:
self.count += 1
return self.count >= self.patience
def _run_epoch(model, loader, criterion, device, optimizer=None, scaler=None):
training = optimizer is not None
model.train(training)
total_loss = correct = top5_sum = n = 0
for images, labels in tqdm(loader, desc="train" if training else "val ", leave=False):
images, labels = images.to(device), labels.to(device)
if training:
optimizer.zero_grad()
if scaler:
with torch.cuda.amp.autocast():
logits = model(images)
loss = criterion(logits, labels)
scaler.scale(loss).backward()
scaler.unscale_(optimizer)
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
scaler.step(optimizer)
scaler.update()
else:
logits = model(images)
loss = criterion(logits, labels)
loss.backward()
nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
else:
with torch.no_grad():
logits = model(images)
loss = criterion(logits, labels)
total_loss += loss.item()
correct += (logits.detach().argmax(1) == labels).sum().item()
top5_sum += _top5(logits.detach(), labels) * labels.size(0)
n += labels.size(0)
return total_loss / len(loader), correct / n, top5_sum / n
def train(config_path: str = "configs/config.yaml"):
with open(config_path) as f:
config = yaml.safe_load(f)
seed_everything(42)
device = (
"cuda" if torch.cuda.is_available()
else "mps" if torch.backends.mps.is_available()
else "cpu"
)
print(f"Device: {device}")
train_loader, val_loader, classes = build_dataloaders(config)
config["model"]["num_classes"] = len(classes)
print(f"Classes: {len(classes)}")
model = build_model(config).to(device)
criterion = nn.CrossEntropyLoss(label_smoothing=0.1)
optimizer = AdamW(model.parameters(), lr=config["training"]["lr"], weight_decay=config["training"]["weight_decay"])
scheduler = CosineAnnealingLR(optimizer, T_max=config["training"]["epochs"])
stopper = EarlyStopping(config["training"]["early_stopping_patience"])
scaler = torch.cuda.amp.GradScaler() if device == "cuda" else None
use_wandb = config["logging"]["use_wandb"]
if use_wandb:
import wandb
wandb.init(project=config["logging"]["project"], config=config)
ckpt_dir = Path(config["paths"]["checkpoints"])
ckpt_dir.mkdir(parents=True, exist_ok=True)
(ckpt_dir / "classes.json").write_text(json.dumps(classes))
best_acc = 0.0
for epoch in range(1, config["training"]["epochs"] + 1):
t0 = time.time()
tr_loss, tr_acc, _ = _run_epoch(model, train_loader, criterion, device, optimizer, scaler)
vl_loss, vl_acc, vl_top5 = _run_epoch(model, val_loader, criterion, device)
scheduler.step()
print(
f"[{epoch:03d}] train loss={tr_loss:.4f} acc={tr_acc:.3f} | "
f"val loss={vl_loss:.4f} acc={vl_acc:.3f} top5={vl_top5:.3f} | "
f"{time.time() - t0:.0f}s"
)
if use_wandb:
import wandb
wandb.log({"train/loss": tr_loss, "train/acc": tr_acc,
"val/loss": vl_loss, "val/acc": vl_acc, "val/top5": vl_top5})
if vl_acc > best_acc:
best_acc = vl_acc
torch.save(
{"epoch": epoch, "model_state_dict": model.state_dict(), "config": config, "val_acc": vl_acc},
ckpt_dir / "best.pt",
)
print(f" ✓ saved best (val_acc={vl_acc:.4f})")
if stopper(vl_loss):
print(f"Early stopping at epoch {epoch}")
break
if use_wandb:
import wandb
wandb.finish()
print(f"\nBest val accuracy: {best_acc:.4f}")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--config", default="configs/config.yaml")
args = parser.parse_args()
train(args.config)