Spaces:
Sleeping
Sleeping
| import json | |
| import torch | |
| import torch.nn as nn | |
| from torch.utils.data import DataLoader | |
| from core.config import (BATCH_SIZE, CHECKPOINTS_DIR, EARLY_STOP_PATIENCE, | |
| LEARNING_RATE, MAX_EPOCHS) | |
| from model.architecture import TamperNet | |
| from model.dataset import TamperDataset | |
| from model.evaluate import evaluate | |
| def compute_loss(pred_mask, pred_logit, gt_mask, gt_label, pos_weight: float = 1.0): | |
| # pos_weight scales the POSITIVE (tampered) class. Tampered is the majority | |
| # here, so pos_weight < 1 down-weights it and balances toward genuine. | |
| pw = torch.tensor([pos_weight], device=pred_logit.device) | |
| label_loss = nn.BCEWithLogitsLoss(pos_weight=pw)(pred_logit.view(-1), gt_label.float()) | |
| # Only apply mask loss on tampered samples (label=1); genuine masks are trivially zero | |
| tampered = gt_label.bool() | |
| if tampered.any(): | |
| mask_loss = nn.BCELoss()(pred_mask[tampered], gt_mask[tampered]) | |
| else: | |
| mask_loss = torch.tensor(0.0, device=pred_logit.device) | |
| return label_loss + 0.5 * mask_loss | |
| def train(): | |
| device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') | |
| print(f'Training on {device}') | |
| train_ds = TamperDataset(split='train') | |
| val_ds = TamperDataset(split='val') | |
| train_dl = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=0) | |
| val_dl = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=0) | |
| # Balance the classes: pos_weight = n_genuine / n_tampered | |
| labels = [lbl for _, lbl in train_ds.items] | |
| n_tamper = max(sum(labels), 1) | |
| n_genuine = max(len(labels) - n_tamper, 1) | |
| pos_weight = n_genuine / n_tamper | |
| print(f'Class balance: {n_genuine} genuine / {n_tamper} tampered → pos_weight={pos_weight:.3f}') | |
| model = TamperNet().to(device) | |
| opt = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE) | |
| sched = torch.optim.lr_scheduler.ReduceLROnPlateau(opt, patience=3, factor=0.5) | |
| best_auc = 0.0 | |
| no_improve = 0 | |
| history = [] | |
| CHECKPOINTS_DIR.mkdir(parents=True, exist_ok=True) | |
| for epoch in range(1, MAX_EPOCHS + 1): | |
| model.train() | |
| train_loss = 0.0 | |
| for imgs, masks, labels in train_dl: | |
| imgs, masks, labels = imgs.to(device), masks.to(device), labels.to(device) | |
| opt.zero_grad() | |
| pred_mask, pred_logit = model(imgs) | |
| loss = compute_loss(pred_mask, pred_logit, masks, labels, pos_weight) | |
| loss.backward() | |
| opt.step() | |
| train_loss += loss.item() | |
| metrics = evaluate(model, val_dl, device) | |
| sched.step(metrics['auc']) | |
| print(f"Epoch {epoch:03d} | loss={train_loss/len(train_dl):.4f} " | |
| f"| auc={metrics['auc']:.4f} | iou={metrics['pixel_iou']:.4f}") | |
| history.append({'epoch': epoch, **metrics}) | |
| if metrics['auc'] > best_auc: | |
| best_auc = metrics['auc'] | |
| no_improve = 0 | |
| torch.save(model.state_dict(), CHECKPOINTS_DIR / 'best.pt') | |
| print(f' saved best model (auc={best_auc:.4f})') | |
| else: | |
| no_improve += 1 | |
| if no_improve >= EARLY_STOP_PATIENCE: | |
| print(f'Early stopping at epoch {epoch}') | |
| break | |
| # Always save final weights so inference has something to load | |
| torch.save(model.state_dict(), CHECKPOINTS_DIR / 'last.pt') | |
| if not (CHECKPOINTS_DIR / 'best.pt').exists(): | |
| torch.save(model.state_dict(), CHECKPOINTS_DIR / 'best.pt') | |
| print('Saved best.pt (fallback — auc never exceeded 0.0)') | |
| with open(CHECKPOINTS_DIR / 'history.json', 'w') as f: | |
| json.dump(history, f, indent=2) | |
| print(f'Training done. Best AUC: {best_auc:.4f}') |